diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations')
71 files changed, 3458 insertions, 1918 deletions
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index 3239b20b2..fae78b9bc 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -30,12 +30,12 @@ namespace MediaBrowser.Server.Implementations.Channels return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>()); } - public Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken) + public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken) + public Task CloseMediaSource(string liveStreamId) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index b76cf46b0..1369efae1 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -1577,97 +1577,5 @@ namespace MediaBrowser.Server.Implementations.Channels return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false); } - - public async Task DownloadChannelItem(BaseItem item, string destination, - IProgress<double> progress, CancellationToken cancellationToken) - { - var sources = await GetDynamicMediaSources(item, cancellationToken) - .ConfigureAwait(false); - - var list = sources.Where(i => i.Protocol == MediaProtocol.Http).ToList(); - - foreach (var source in list) - { - await TryDownloadChannelItem(source, item, destination, progress, cancellationToken).ConfigureAwait(false); - return; - } - } - - private async Task TryDownloadChannelItem(MediaSourceInfo source, - BaseItem item, - string destination, - IProgress<double> progress, - CancellationToken cancellationToken) - { - var options = new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = source.Path, - Progress = new Progress<double>() - }; - - var channel = GetChannel(item.ChannelId); - var channelProvider = GetChannelProvider(channel); - var features = channelProvider.GetChannelFeatures(); - - if (!features.SupportsContentDownloading) - { - throw new ArgumentException("The channel does not support downloading."); - } - - var limit = features.DailyDownloadLimit; - - foreach (var header in source.RequiredHttpHeaders) - { - options.RequestHeaders[header.Key] = header.Value; - } - - _fileSystem.CreateDirectory(Path.GetDirectoryName(destination)); - - // Determine output extension - var response = await _httpClient.GetTempFileResponse(options).ConfigureAwait(false); - - if (response.ContentType.StartsWith("text/html")) - { - throw new HttpException("File not found") - { - StatusCode = HttpStatusCode.NotFound - }; - } - - if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) - { - var extension = response.ContentType.Split('/') - .Last() - .Replace("quicktime", "mov", StringComparison.OrdinalIgnoreCase); - - destination += "." + extension; - } - else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase)) - { - var extension = response.ContentType.Replace("audio/mpeg", "audio/mp3", StringComparison.OrdinalIgnoreCase) - .Split('/') - .Last(); - - destination += "." + extension; - } - else - { - _fileSystem.DeleteFile(response.TempFilePath); - - throw new ApplicationException("Unexpected response type encountered: " + response.ContentType); - } - - _fileSystem.CopyFile(response.TempFilePath, destination, true); - - try - { - _fileSystem.DeleteFile(response.TempFilePath); - } - catch - { - - } - } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 005bbb852..5ac3da3db 100644 --- a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.Channels public string Category { - get { return "Channels"; } + get { return "Internet Channels"; } } public async Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress) diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 1b6c44c5e..cb2bd645d 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -219,7 +219,9 @@ namespace MediaBrowser.Server.Implementations.Collections foreach (var itemId in itemIds) { - var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value == itemId); + var childItem = _libraryManager.GetItemById(itemId); + + var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == itemId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); if (child == null) { @@ -228,47 +230,15 @@ namespace MediaBrowser.Server.Implementations.Collections list.Add(child); - var childItem = _libraryManager.GetItemById(itemId); - if (childItem != null) { itemList.Add(childItem); } } - var shortcutFiles = _fileSystem - .GetFilePaths(collection.Path) - .Where(i => _fileSystem.IsShortcut(i)) - .ToList(); - - var shortcutFilesToDelete = list.Where(child => !string.IsNullOrWhiteSpace(child.Path) && child.Type == LinkedChildType.Shortcut) - .Select(child => shortcutFiles.FirstOrDefault(i => string.Equals(child.Path, _fileSystem.ResolveShortcut(i), StringComparison.OrdinalIgnoreCase))) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); - - foreach (var file in shortcutFilesToDelete) - { - _iLibraryMonitor.ReportFileSystemChangeBeginning(file); - } - - try - { - foreach (var file in shortcutFilesToDelete) - { - _fileSystem.DeleteFile(file); - } - - foreach (var child in list) - { - collection.LinkedChildren.Remove(child); - } - } - finally + foreach (var child in list) { - foreach (var file in shortcutFilesToDelete) - { - _iLibraryMonitor.ReportFileSystemChangeComplete(file, false); - } + collection.LinkedChildren.Remove(child); } collection.UpdateRatingToContent(); diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs index 2b2373a47..f9eff3c92 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs @@ -127,7 +127,8 @@ namespace MediaBrowser.Server.Implementations.Connect // Seeing block length errors with our server EnableHttpCompression = false, - PreferIpv4 = preferIpv4 + PreferIpv4 = preferIpv4, + BufferContent = false }).ConfigureAwait(false)) { diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs index 45cb2f57d..d7c1b0da0 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs @@ -266,7 +266,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; options.SetPostData(postData); @@ -314,7 +315,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; options.SetPostData(postData); @@ -403,6 +405,14 @@ namespace MediaBrowser.Server.Implementations.Connect public async Task<UserLinkResult> LinkUser(string userId, string connectUsername) { + if (string.IsNullOrWhiteSpace(userId)) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrWhiteSpace(connectUsername)) + { + throw new ArgumentNullException("connectUsername"); + } if (string.IsNullOrWhiteSpace(ConnectServerId)) { await UpdateConnectInfo().ConfigureAwait(false); @@ -422,14 +432,6 @@ namespace MediaBrowser.Server.Implementations.Connect private async Task<UserLinkResult> LinkUserInternal(string userId, string connectUsername) { - if (string.IsNullOrWhiteSpace(userId)) - { - throw new ArgumentNullException("userId"); - } - if (string.IsNullOrWhiteSpace(connectUsername)) - { - throw new ArgumentNullException("connectUsername"); - } if (string.IsNullOrWhiteSpace(ConnectServerId)) { throw new ArgumentNullException("ConnectServerId"); @@ -446,11 +448,17 @@ namespace MediaBrowser.Server.Implementations.Connect throw new ArgumentException("The Emby account has been disabled."); } + var existingUser = _userManager.Users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUser.Id) && !string.IsNullOrWhiteSpace(i.ConnectAccessKey)); + if (existingUser != null) + { + throw new InvalidOperationException("This connect user is already linked to local user " + existingUser.Name); + } + var user = GetUser(userId); if (!string.IsNullOrWhiteSpace(user.ConnectUserId)) { - await RemoveConnect(user, connectUser.Id).ConfigureAwait(false); + await RemoveConnect(user, user.ConnectUserId).ConfigureAwait(false); } var url = GetConnectUrl("ServerAuthorizations"); @@ -458,7 +466,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; var accessToken = Guid.NewGuid().ToString("N"); @@ -593,7 +602,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; var accessToken = Guid.NewGuid().ToString("N"); @@ -646,7 +656,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; var postData = new Dictionary<string, string> @@ -720,7 +731,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { CancellationToken = cancellationToken, - Url = url + Url = url, + BufferContent = false }; SetServerAccessToken(options); @@ -784,7 +796,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + BufferContent = false }; SetServerAccessToken(options); @@ -1072,7 +1085,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { Url = url, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false }; var postData = new Dictionary<string, string> @@ -1120,7 +1134,8 @@ namespace MediaBrowser.Server.Implementations.Connect var options = new HttpRequestOptions { - Url = GetConnectUrl("user/authenticate") + Url = GetConnectUrl("user/authenticate"), + BufferContent = false }; options.SetPostData(new Dictionary<string, string> diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 9284f4fc7..a0f7aa999 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -437,6 +437,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.TrailerCount = taggedItems.Count(i => i is Trailer); dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo); dto.SeriesCount = taggedItems.Count(i => i is Series); + dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram); dto.SongCount = taggedItems.Count(i => i is Audio); } @@ -597,7 +598,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.Altitude = item.Altitude; dto.IsoSpeedRating = item.IsoSpeedRating; - var album = item.Album; + var album = item.AlbumEntity; if (album != null) { @@ -906,11 +907,6 @@ namespace MediaBrowser.Server.Implementations.Dto dto.Keywords = item.Keywords; } - if (fields.Contains(ItemFields.ProductionLocations)) - { - SetProductionLocations(item, dto); - } - var hasAspectRatio = item as IHasAspectRatio; if (hasAspectRatio != null) { @@ -997,8 +993,11 @@ namespace MediaBrowser.Server.Implementations.Dto } dto.Audio = item.Audio; - dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; - dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; + if (fields.Contains(ItemFields.Settings)) + { + dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; + dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; + } dto.CriticRating = item.CriticRating; @@ -1088,10 +1087,9 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.Taglines)) { - var hasTagline = item as IHasTaglines; - if (hasTagline != null) + if (!string.IsNullOrWhiteSpace(item.Tagline)) { - dto.Taglines = hasTagline.Taglines; + dto.Taglines = new List<string> { item.Tagline }; } if (dto.Taglines == null) @@ -1175,6 +1173,12 @@ namespace MediaBrowser.Server.Implementations.Dto .Except(foundArtists, new DistinctNameComparer()) .Select(i => { + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrWhiteSpace(i)) + { + return null; + } + var artist = _libraryManager.GetArtist(i); if (artist != null) { @@ -1419,6 +1423,11 @@ namespace MediaBrowser.Server.Implementations.Dto SetBookProperties(dto, book); } + if (item.ProductionLocations.Count > 0 || item is Movie) + { + dto.ProductionLocations = item.ProductionLocations.ToArray(); + } + var photo = item as Photo; if (photo != null) { @@ -1508,7 +1517,7 @@ namespace MediaBrowser.Server.Implementations.Dto } } - private string GetMappedPath(IHasMetadata item) + private string GetMappedPath(BaseItem item) { var path = item.Path; @@ -1516,40 +1525,12 @@ namespace MediaBrowser.Server.Implementations.Dto if (locationType == LocationType.FileSystem || locationType == LocationType.Offline) { - foreach (var map in _config.Configuration.PathSubstitutions) - { - path = _libraryManager.SubstitutePath(path, map.From, map.To); - } + path = _libraryManager.GetPathAfterNetworkSubstitution(path, item); } return path; } - private void SetProductionLocations(BaseItem item, BaseItemDto dto) - { - var hasProductionLocations = item as IHasProductionLocations; - - if (hasProductionLocations != null) - { - dto.ProductionLocations = hasProductionLocations.ProductionLocations; - } - - var person = item as Person; - if (person != null) - { - dto.ProductionLocations = new List<string>(); - if (!string.IsNullOrEmpty(person.PlaceOfBirth)) - { - dto.ProductionLocations.Add(person.PlaceOfBirth); - } - } - - if (dto.ProductionLocations == null) - { - dto.ProductionLocations = new List<string>(); - } - } - /// <summary> /// Attaches the primary image aspect ratio. /// </summary> diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 280bec65b..1021d8823 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net; using MediaBrowser.Common.Threading; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.EntryPoints { @@ -17,17 +18,17 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private readonly ISsdpHandler _ssdp; + private readonly IDeviceDiscovery _deviceDiscovery; private PeriodicTimer _timer; private bool _isStarted; - public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, ISsdpHandler ssdp) + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery) { _logger = logmanager.GetLogger("PortMapper"); _appHost = appHost; _config = config; - _ssdp = ssdp; + _deviceDiscovery = deviceDiscovery; } private string _lastConfigIdentifier; @@ -61,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints public void Run() { - //NatUtility.Logger = new LogWriter(_logger); + NatUtility.Logger = _logger; if (_config.Configuration.EnableUPnP) { @@ -93,33 +94,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - _ssdp.MessageReceived += _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; _lastConfigIdentifier = GetConfigIdentifier(); _isStarted = true; } - private void ClearCreatedRules(object state) - { - _createdRules = new List<string>(); - _usnsHandled = new List<string>(); - } - - void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e) + private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { - var endpoint = e.EndPoint as IPEndPoint; - - if (endpoint == null || e.LocalEndPoint == null) - { - return; - } + var info = e.Argument; string usn; - if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty; string nt; - if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty; // Filter device type if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && @@ -132,15 +122,45 @@ namespace MediaBrowser.Server.Implementations.EntryPoints var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; - if (!_usnsHandled.Contains(identifier)) + if (info.Location != null && !_usnsHandled.Contains(identifier)) { _usnsHandled.Add(identifier); _logger.Debug("Calling Nat.Handle on " + identifier); - NatUtility.Handle(e.LocalEndPoint.Address, e.Message, endpoint, NatProtocol.Upnp); + + IPAddress address; + if (IPAddress.TryParse(info.Location.Host, out address)) + { + // The Handle method doesn't need the port + var endpoint = new IPEndPoint(address, info.Location.Port); + + IPAddress localAddress = null; + + try + { + var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } + } + catch + { + return; + } + + NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); + } } } + private void ClearCreatedRules(object state) + { + _createdRules = new List<string>(); + _usnsHandled = new List<string>(); + } + void NatUtility_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var ex = e.ExceptionObject as Exception; @@ -228,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _timer = null; } - _ssdp.MessageReceived -= _ssdp_MessageReceived; + _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; try { diff --git a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs index e84b66c5a..f7fe707da 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs @@ -377,10 +377,10 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications DisposeLibraryUpdateTimer(); } - if (items.Count == 1) - { - var item = items.First(); + items = items.Take(10).ToList(); + foreach (var item in items) + { var notification = new NotificationRequest { NotificationType = NotificationType.NewLibraryContent.ToString() @@ -390,17 +390,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications await SendNotification(notification).ConfigureAwait(false); } - else - { - var notification = new NotificationRequest - { - NotificationType = NotificationType.NewLibraryContentMultiple.ToString() - }; - - notification.Variables["ItemCount"] = items.Count.ToString(CultureInfo.InvariantCulture); - - await SendNotification(notification).ConfigureAwait(false); - } } public static string GetItemName(BaseItem item) diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs index 7b3a7a30d..e445300e4 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs @@ -64,7 +64,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints EnableHttpCompression = false, LogRequest = false, - LogErrors = logErrors + LogErrors = logErrors, + BufferContent = false }; options.SetPostData(data); @@ -114,7 +115,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints EnableHttpCompression = false, LogRequest = false, - LogErrors = logErrors + LogErrors = logErrors, + BufferContent = false }; options.SetPostData(data); diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 8e46f8f03..9ec671908 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -19,6 +19,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Common.Security; using MediaBrowser.Model.Extensions; @@ -45,16 +46,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; + private readonly IMemoryStreamProvider _memoryStreamProvider; public HttpListenerHost(IApplicationHost applicationHost, ILogManager logManager, IServerConfigurationManager config, string serviceName, - string defaultRedirectPath, INetworkManager networkManager, params Assembly[] assembliesWithServices) + string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamProvider memoryStreamProvider, params Assembly[] assembliesWithServices) : base(serviceName, assembliesWithServices) { DefaultRedirectPath = defaultRedirectPath; _networkManager = networkManager; + _memoryStreamProvider = memoryStreamProvider; _config = config; _logger = logManager.GetLogger("HttpServer"); @@ -88,14 +91,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer HostConfig.Instance.DebugMode = false; HostConfig.Instance.LogFactory = LogManager.LogFactory; + HostConfig.Instance.AllowJsonpRequests = false; // The Markdown feature causes slow startup times (5 mins+) on cold boots for some users // Custom format allows images - HostConfig.Instance.EnableFeatures = Feature.Csv | Feature.Html | Feature.Json | Feature.Jsv | Feature.Metadata | Feature.Xml | Feature.CustomFormat; + HostConfig.Instance.EnableFeatures = Feature.Html | Feature.Json | Feature.Xml | Feature.CustomFormat; container.Adapter = _containerAdapter; - Plugins.Add(new SwaggerFeature()); + Plugins.RemoveAll(x => x is NativeTypesFeature); + //Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization")); //Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] { @@ -179,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer private IHttpListener GetListener() { - return new WebSocketSharpListener(_logger, CertificatePath); + return new WebSocketSharpListener(_logger, CertificatePath, _memoryStreamProvider); } private void OnWebSocketConnecting(WebSocketConnectingEventArgs args) @@ -542,8 +547,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - - throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); + else + { + httpRes.Close(); + } } /// <summary> diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs index c55e98388..10d6f7493 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -93,12 +93,14 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - - if (responseHeaders != null) + if (responseHeaders == null) { - AddResponseHeaders(result, responseHeaders); + responseHeaders = new Dictionary<string, string>(); } + responseHeaders["Expires"] = "-1"; + AddResponseHeaders(result, responseHeaders); + return result; } @@ -681,29 +683,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - /// <summary> - /// Gets the error result. - /// </summary> - /// <param name="statusCode">The status code.</param> - /// <param name="errorMessage">The error message.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - public void ThrowError(int statusCode, string errorMessage, IDictionary<string, string> responseHeaders = null) - { - var error = new HttpError - { - Status = statusCode, - ErrorCode = errorMessage - }; - - if (responseHeaders != null) - { - AddResponseHeaders(error, responseHeaders); - } - - throw error; - } - public object GetAsyncStreamWriter(IAsyncStreamSource streamSource) { return new AsyncStreamWriter(streamSource); diff --git a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs deleted file mode 100644 index cac2f8e09..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs +++ /dev/null @@ -1,240 +0,0 @@ -using MediaBrowser.Common.Events; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WebSocketMessageType = MediaBrowser.Model.Net.WebSocketMessageType; -using WebSocketState = MediaBrowser.Model.Net.WebSocketState; - -namespace MediaBrowser.Server.Implementations.HttpServer -{ - /// <summary> - /// Class NativeWebSocket - /// </summary> - public class NativeWebSocket : IWebSocket - { - /// <summary> - /// The logger - /// </summary> - private readonly ILogger _logger; - - public event EventHandler<EventArgs> Closed; - - /// <summary> - /// Gets or sets the web socket. - /// </summary> - /// <value>The web socket.</value> - private System.Net.WebSockets.WebSocket WebSocket { get; set; } - - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - - /// <summary> - /// Initializes a new instance of the <see cref="NativeWebSocket" /> class. - /// </summary> - /// <param name="socket">The socket.</param> - /// <param name="logger">The logger.</param> - /// <exception cref="System.ArgumentNullException">socket</exception> - public NativeWebSocket(WebSocket socket, ILogger logger) - { - if (socket == null) - { - throw new ArgumentNullException("socket"); - } - - if (logger == null) - { - throw new ArgumentNullException("logger"); - } - - _logger = logger; - WebSocket = socket; - - Receive(); - } - - /// <summary> - /// Gets or sets the state. - /// </summary> - /// <value>The state.</value> - public WebSocketState State - { - get - { - WebSocketState commonState; - - if (!Enum.TryParse(WebSocket.State.ToString(), true, out commonState)) - { - _logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.State.ToString()); - } - - return commonState; - } - } - - /// <summary> - /// Receives this instance. - /// </summary> - private async void Receive() - { - while (true) - { - byte[] bytes; - - try - { - bytes = await ReceiveBytesAsync(_cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (WebSocketException ex) - { - _logger.ErrorException("Error receiving web socket message", ex); - - break; - } - - if (bytes == null) - { - // Connection closed - EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger); - break; - } - - if (OnReceiveBytes != null) - { - OnReceiveBytes(bytes); - } - } - } - - /// <summary> - /// Receives the async. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{WebSocketMessageInfo}.</returns> - /// <exception cref="System.Net.WebSockets.WebSocketException">Connection closed</exception> - private async Task<byte[]> ReceiveBytesAsync(CancellationToken cancellationToken) - { - var bytes = new byte[4096]; - var buffer = new ArraySegment<byte>(bytes); - - var result = await WebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); - - if (result.CloseStatus.HasValue) - { - _logger.Info("Web socket connection closed by client. Reason: {0}", result.CloseStatus.Value); - return null; - } - - return buffer.Array; - } - - /// <summary> - /// Sends the async. - /// </summary> - /// <param name="bytes">The bytes.</param> - /// <param name="type">The type.</param> - /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken) - { - System.Net.WebSockets.WebSocketMessageType nativeType; - - if (!Enum.TryParse(type.ToString(), true, out nativeType)) - { - _logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString()); - } - - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - return WebSocket.SendAsync(new ArraySegment<byte>(bytes), nativeType, true, linkedTokenSource.Token); - } - - public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) - { - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Binary, true, linkedTokenSource.Token); - } - - public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) - { - var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token); - - var bytes = Encoding.UTF8.GetBytes(text); - - return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, linkedTokenSource.Token); - } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - } - - /// <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) - { - _cancellationTokenSource.Cancel(); - - WebSocket.Dispose(); - } - } - - /// <summary> - /// Gets or sets the receive action. - /// </summary> - /// <value>The receive action.</value> - public Action<byte[]> OnReceiveBytes { get; set; } - - /// <summary> - /// Gets or sets the on receive. - /// </summary> - /// <value>The on receive.</value> - public Action<string> OnReceive { get; set; } - - /// <summary> - /// The _supports native web socket - /// </summary> - private static bool? _supportsNativeWebSocket; - - /// <summary> - /// Gets a value indicating whether [supports web sockets]. - /// </summary> - /// <value><c>true</c> if [supports web sockets]; otherwise, <c>false</c>.</value> - public static bool IsSupported - { - get - { - if (!_supportsNativeWebSocket.HasValue) - { - try - { - new ClientWebSocket(); - - _supportsNativeWebSocket = true; - } - catch (PlatformNotSupportedException) - { - _supportsNativeWebSocket = false; - } - } - - return _supportsNativeWebSocket.Value; - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs index 488c630fe..4b94095f5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -191,15 +191,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - catch (IOException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error in range request writer", ex); - throw; - } finally { if (OnComplete != null) @@ -251,15 +242,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } } - catch (IOException ex) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error in range request writer", ex); - throw; - } finally { if (OnComplete != null) diff --git a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs index cc351f6b3..8a7c14eb6 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs @@ -1,4 +1,5 @@ using MediaBrowser.Common; +using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; @@ -15,23 +16,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// <summary> /// Creates the server. /// </summary> - /// <param name="applicationHost">The application host.</param> - /// <param name="logManager">The log manager.</param> - /// <param name="config">The configuration.</param> - /// <param name="_networkmanager">The _networkmanager.</param> - /// <param name="serverName">Name of the server.</param> - /// <param name="defaultRedirectpath">The default redirectpath.</param> /// <returns>IHttpServer.</returns> public static IHttpServer CreateServer(IApplicationHost applicationHost, ILogManager logManager, IServerConfigurationManager config, INetworkManager _networkmanager, + IMemoryStreamProvider streamProvider, string serverName, string defaultRedirectpath) { LogManager.LogFactory = new ServerLogFactory(logManager); - return new HttpListenerHost(applicationHost, logManager, config, serverName, defaultRedirectpath, _networkmanager); + return new HttpListenerHost(applicationHost, logManager, config, serverName, defaultRedirectpath, _networkmanager, streamProvider); } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs index bfa65ac6b..d20dd7ec0 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs @@ -39,11 +39,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp if (boundary == null) return; - using (var requestStream = GetSubStream(InputStream)) + using (var requestStream = GetSubStream(InputStream, _memoryStreamProvider)) { //DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request //Not ending with \r\n? - var ms = new MemoryStream(32 * 1024); + var ms = _memoryStreamProvider.CreateNew(32 * 1024); await requestStream.CopyToAsync(ms).ConfigureAwait(false); var input = ms; @@ -229,9 +229,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp async Task LoadWwwForm() { - using (Stream input = GetSubStream(InputStream)) + using (Stream input = GetSubStream(InputStream, _memoryStreamProvider)) { - using (var ms = new MemoryStream()) + using (var ms = _memoryStreamProvider.CreateNew()) { await input.CopyToAsync(ms).ConfigureAwait(false); ms.Position = 0; diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index bcc081eb1..b090c97c6 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { @@ -18,11 +19,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp private readonly ILogger _logger; private readonly string _certificatePath; + private readonly IMemoryStreamProvider _memoryStreamProvider; - public WebSocketSharpListener(ILogger logger, string certificatePath) + public WebSocketSharpListener(ILogger logger, string certificatePath, IMemoryStreamProvider memoryStreamProvider) { _logger = logger; _certificatePath = certificatePath; + _memoryStreamProvider = memoryStreamProvider; } public Action<Exception, IRequest> ErrorHandler { get; set; } @@ -148,7 +151,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { var operationName = httpContext.Request.GetOperationName(); - var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger); + var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger, _memoryStreamProvider); req.RequestAttributes = req.GetAttributes(); return req; diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs index dc2aec3e1..b5c8d0107 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using Funq; +using MediaBrowser.Common.IO; using MediaBrowser.Model.Logging; using ServiceStack; using ServiceStack.Host; @@ -16,11 +17,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public Container Container { get; set; } private readonly HttpListenerRequest request; private readonly IHttpResponse response; + private readonly IMemoryStreamProvider _memoryStreamProvider; - public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger) + public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger, IMemoryStreamProvider memoryStreamProvider) { this.OperationName = operationName; this.RequestAttributes = requestAttributes; + _memoryStreamProvider = memoryStreamProvider; this.request = httpContext.Request; this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); @@ -403,7 +406,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp set { bufferedStream = value - ? bufferedStream ?? new MemoryStream(request.InputStream.ReadFully()) + ? bufferedStream ?? _memoryStreamProvider.CreateNew(request.InputStream.ReadFully()) : null; } } @@ -447,7 +450,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } } - static Stream GetSubStream(Stream stream) + static Stream GetSubStream(Stream stream, IMemoryStreamProvider streamProvider) { if (stream is MemoryStream) { diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index e08be8bd1..a58645ec5 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -81,20 +81,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public void Write(string text) { - try - { - var bOutput = System.Text.Encoding.UTF8.GetBytes(text); - response.ContentLength64 = bOutput.Length; + var bOutput = System.Text.Encoding.UTF8.GetBytes(text); + response.ContentLength64 = bOutput.Length; - var outputStream = response.OutputStream; - outputStream.Write(bOutput, 0, bOutput.Length); - Close(); - } - catch (Exception ex) - { - _logger.ErrorException("Could not WriteTextToResponse: " + ex.Message, ex); - throw; - } + var outputStream = response.OutputStream; + outputStream.Write(bOutput, 0, bOutput.Length); + Close(); } public void Close() diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs index ae408f8d6..5f122fb96 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading.Tasks; +using MediaBrowser.Common.IO; using ServiceStack; namespace MediaBrowser.Server.Implementations.HttpServer @@ -17,7 +18,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer private ILogger Logger { get; set; } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// <summary> /// Gets or sets the source stream. /// </summary> @@ -39,6 +40,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer public Action OnComplete { get; set; } public Action OnError { get; set; } + private readonly byte[] _bytes; /// <summary> /// Initializes a new instance of the <see cref="StreamWriter" /> class. @@ -73,6 +75,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer public StreamWriter(byte[] source, string contentType, ILogger logger) : this(new MemoryStream(source), contentType, logger) { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + _bytes = source; + Logger = logger; + + Options["Content-Type"] = contentType; + + Options["Content-Length"] = source.Length.ToString(UsCulture); } private const int BufferSize = 81920; @@ -85,9 +98,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer { try { - using (var src = SourceStream) + if (_bytes != null) { - src.CopyTo(responseStream, BufferSize); + responseStream.Write(_bytes, 0, _bytes.Length); + } + else + { + using (var src = SourceStream) + { + src.CopyTo(responseStream, BufferSize); + } } } catch (Exception ex) @@ -114,9 +134,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer { try { - using (var src = SourceStream) + if (_bytes != null) + { + await responseStream.WriteAsync(_bytes, 0, _bytes.Length); + } + else { - await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); + using (var src = SourceStream) + { + await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); + } } } catch (Exception ex) diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index dffaec458..76f0e6a1d 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -172,32 +172,16 @@ namespace MediaBrowser.Server.Implementations.IO Start(); } - private bool EnableLibraryMonitor - { - get - { - switch (ConfigurationManager.Configuration.EnableLibraryMonitor) - { - case AutoOnOff.Auto: - return Environment.OSVersion.Platform == PlatformID.Win32NT; - case AutoOnOff.Enabled: - return true; - default: - return false; - } - } - } - private bool IsLibraryMonitorEnabaled(BaseItem item) { var options = LibraryManager.GetLibraryOptions(item); - if (options != null && options.SchemaVersion >= 1) + if (options != null) { return options.EnableRealtimeMonitor; } - return EnableLibraryMonitor; + return false; } public void Start() diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs index 4a43befed..9b23d5be4 100644 --- a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs +++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -99,8 +99,9 @@ namespace MediaBrowser.Server.Implementations.Intros IncludeItemTypes = new[] { typeof(Trailer).Name }, TrailerTypes = trailerTypes.ToArray(), SimilarTo = item, - IsPlayed = config.EnableIntrosForWatchedContent ? (bool?) null : false, + IsPlayed = config.EnableIntrosForWatchedContent ? (bool?)null : false, MaxParentalRating = config.EnableIntrosParentalControl ? ratingLevel : null, + BlockUnratedItems = config.EnableIntrosParentalControl ? new[] { UnratedItem.Trailer } : new UnratedItem[] { }, Limit = config.TrailerLimit }); @@ -110,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.Intros Type = i.SourceType == SourceType.Channel ? ItemWithTrailerType.ChannelTrailer : ItemWithTrailerType.ItemWithTrailer, LibraryManager = _libraryManager })); - } + } return GetResult(item, candidates, config); } @@ -197,7 +198,7 @@ namespace MediaBrowser.Server.Implementations.Intros } returnResult.AddRange(GetMediaInfoIntrosByTags(allIntros, item.Tags).Take(1)); - + return returnResult.DistinctBy(i => i.Path, StringComparer.OrdinalIgnoreCase); } catch (IOException) diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index ba7e33890..b550d1dda 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -33,7 +33,8 @@ namespace MediaBrowser.Server.Implementations.Library // Synology "@eaDir", - "eaDir" + "eaDir", + "#recycle" }; diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 442e2ebe5..b2302cf86 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -43,6 +43,7 @@ using MediaBrowser.Server.Implementations.Library.Resolvers; using MoreLinq; using SortOrder = MediaBrowser.Model.Entities.SortOrder; using VideoResolver = MediaBrowser.Naming.Video.VideoResolver; +using MediaBrowser.Common.Configuration; namespace MediaBrowser.Server.Implementations.Library { @@ -334,15 +335,6 @@ namespace MediaBrowser.Server.Implementations.Library } } - /// <summary> - /// Updates the item in library cache. - /// </summary> - /// <param name="item">The item.</param> - private void UpdateItemInLibraryCache(BaseItem item) - { - RegisterItem(item); - } - public void RegisterItem(BaseItem item) { if (item == null) @@ -695,7 +687,7 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, - Folder parent, + Folder parent, LibraryOptions libraryOptions, string collectionType, IItemResolver[] resolvers) @@ -1216,12 +1208,7 @@ namespace MediaBrowser.Server.Implementations.Library if (libraryFolder != null) { info.ItemId = libraryFolder.Id.ToString("N"); - } - - var collectionFolder = libraryFolder as CollectionFolder; - if (collectionFolder != null) - { - info.LibraryOptions = collectionFolder.GetLibraryOptions(); + info.LibraryOptions = GetLibraryOptions(libraryFolder); } return info; @@ -1490,10 +1477,10 @@ namespace MediaBrowser.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user) { - if (query.AncestorIds.Length == 0 && - !query.ParentId.HasValue && - query.ChannelIds.Length == 0 && - query.TopParentIds.Length == 0 && + if (query.AncestorIds.Length == 0 && + !query.ParentId.HasValue && + query.ChannelIds.Length == 0 && + query.TopParentIds.Length == 0 && string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) && query.ItemIds.Length == 0) { @@ -1781,7 +1768,7 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var item in list) { - UpdateItemInLibraryCache(item); + RegisterItem(item); } if (ItemAdded != null) @@ -1822,7 +1809,7 @@ namespace MediaBrowser.Server.Implementations.Library await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false); - UpdateItemInLibraryCache(item); + RegisterItem(item); if (ItemUpdated != null) { @@ -1889,11 +1876,41 @@ namespace MediaBrowser.Server.Implementations.Library public LibraryOptions GetLibraryOptions(BaseItem item) { - var collectionFolder = GetCollectionFolders(item) - .OfType<CollectionFolder>() - .FirstOrDefault(); + var collectionFolder = item as CollectionFolder; + if (collectionFolder == null) + { + collectionFolder = GetCollectionFolders(item) + .OfType<CollectionFolder>() + .FirstOrDefault(); + } + + var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + + if (options.SchemaVersion < 3) + { + options.SaveLocalMetadata = ConfigurationManager.Configuration.SaveLocalMeta; + options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders; + } + + if (options.SchemaVersion < 2) + { + var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters"); + options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan; - return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); + if (collectionFolder != null) + { + if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase)) + { + options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction; + } + else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + { + options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction; + } + } + } + + return options; } public string GetContentType(BaseItem item) @@ -2437,7 +2454,7 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) + var files = owner.DetectIsInMixedFolder() ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) .Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); @@ -2527,8 +2544,60 @@ namespace MediaBrowser.Server.Implementations.Library }).OrderBy(i => i.Path).ToList(); } + public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem) + { + if (ownerItem != null) + { + var libraryOptions = GetLibraryOptions(ownerItem); + if (libraryOptions != null) + { + foreach (var pathInfo in libraryOptions.PathInfos) + { + if (string.IsNullOrWhiteSpace(pathInfo.NetworkPath)) + { + continue; + } + + var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath); + if (substitutionResult.Item2) + { + return substitutionResult.Item1; + } + } + } + } + + var metadataPath = ConfigurationManager.Configuration.MetadataPath; + var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath; + + if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath)) + { + var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath); + if (metadataSubstitutionResult.Item2) + { + return metadataSubstitutionResult.Item1; + } + } + + foreach (var map in ConfigurationManager.Configuration.PathSubstitutions) + { + var substitutionResult = SubstitutePathInternal(path, map.From, map.To); + if (substitutionResult.Item2) + { + return substitutionResult.Item1; + } + } + + return path; + } + public string SubstitutePath(string path, string from, string to) { + return SubstitutePathInternal(path, from, to).Item1; + } + + private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to) + { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); @@ -2542,7 +2611,11 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("to"); } + from = from.Trim(); + to = to.Trim(); + var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase); + var changed = false; if (!string.Equals(newPath, path)) { @@ -2554,9 +2627,11 @@ namespace MediaBrowser.Server.Implementations.Library { newPath = newPath.Replace('/', '\\'); } + + changed = true; } - return newPath; + return new Tuple<string, bool>(newPath, changed); } private void SetExtraTypeFromFilename(Video item) @@ -2685,7 +2760,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new InvalidOperationException(); } - public void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, LibraryOptions options, bool refreshLibrary) + public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) { @@ -2703,12 +2778,13 @@ namespace MediaBrowser.Server.Implementations.Library virtualFolderPath = Path.Combine(rootFolderPath, name); } - if (mediaPaths != null) + var mediaPathInfos = options.PathInfos; + if (mediaPathInfos != null) { - var invalidpath = mediaPaths.FirstOrDefault(i => !_fileSystem.DirectoryExists(i)); + var invalidpath = mediaPathInfos.FirstOrDefault(i => !_fileSystem.DirectoryExists(i.Path)); if (invalidpath != null) { - throw new ArgumentException("The specified path does not exist: " + invalidpath + "."); + throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + "."); } } @@ -2730,11 +2806,11 @@ namespace MediaBrowser.Server.Implementations.Library CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); - if (mediaPaths != null) + if (mediaPathInfos != null) { - foreach (var path in mediaPaths) + foreach (var path in mediaPathInfos) { - AddMediaPath(name, path); + AddMediaPathInternal(name, path, false); } } } @@ -2760,6 +2836,137 @@ namespace MediaBrowser.Server.Implementations.Library } } + private bool ValidateNetworkPath(string path) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase)) + { + return Directory.Exists(path); + } + + // Without native support for unc, we cannot validate this when running under mono + return true; + } + + private const string ShortcutFileExtension = ".mblink"; + private const string ShortcutFileSearch = "*" + ShortcutFileExtension; + public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + { + AddMediaPathInternal(virtualFolderName, pathInfo, true); + } + + private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions) + { + if (pathInfo == null) + { + throw new ArgumentNullException("path"); + } + + var path = pathInfo.Path; + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + + if (!_fileSystem.DirectoryExists(path)) + { + throw new DirectoryNotFoundException("The path does not exist."); + } + + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) + { + throw new DirectoryNotFoundException("The network path does not exist."); + } + + var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); + + var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path); + + var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + + while (_fileSystem.FileExists(lnk)) + { + shortcutFilename += "1"; + lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + } + + _fileSystem.CreateShortcut(lnk, path); + + RemoveContentTypeOverrides(path); + + if (saveLibraryOptions) + { + var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); + + var list = libraryOptions.PathInfos.ToList(); + list.Add(pathInfo); + libraryOptions.PathInfos = list.ToArray(); + + SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); + + CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); + } + } + + public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo) + { + if (pathInfo == null) + { + throw new ArgumentNullException("path"); + } + + if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) + { + throw new DirectoryNotFoundException("The network path does not exist."); + } + + var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); + + var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); + + SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions); + + var list = libraryOptions.PathInfos.ToList(); + foreach (var originalPathInfo in list) + { + if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal)) + { + originalPathInfo.NetworkPath = pathInfo.NetworkPath; + break; + } + } + + libraryOptions.PathInfos = list.ToArray(); + + CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); + } + + private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options) + { + var topLibraryFolders = GetUserRootFolder().Children.ToList(); + var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders); + + if (info.Locations.Count > 0 && info.Locations.Count != options.PathInfos.Length) + { + var list = options.PathInfos.ToList(); + + foreach (var location in info.Locations) + { + if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal))) + { + list.Add(new MediaPathInfo + { + Path = location + }); + } + } + + options.PathInfos = list.ToArray(); + } + } + public void RemoveVirtualFolder(string name, bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) @@ -2804,38 +3011,6 @@ namespace MediaBrowser.Server.Implementations.Library } } - private const string ShortcutFileExtension = ".mblink"; - private const string ShortcutFileSearch = "*" + ShortcutFileExtension; - public void AddMediaPath(string virtualFolderName, string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException("path"); - } - - if (!_fileSystem.DirectoryExists(path)) - { - throw new DirectoryNotFoundException("The path does not exist."); - } - - var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; - var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - - var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path); - - var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - - while (_fileSystem.FileExists(lnk)) - { - shortcutFilename += "1"; - lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - } - - _fileSystem.CreateShortcut(lnk, path); - - RemoveContentTypeOverrides(path); - } - private void RemoveContentTypeOverrides(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -2872,19 +3047,28 @@ namespace MediaBrowser.Server.Implementations.Library } var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; - var path = Path.Combine(rootFolderPath, virtualFolderName); + var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - if (!_fileSystem.DirectoryExists(path)) + if (!_fileSystem.DirectoryExists(virtualFolderPath)) { throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); } - var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); + var shortcut = Directory.EnumerateFiles(virtualFolderPath, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) { _fileSystem.DeleteFile(shortcut); } + + var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath); + + libraryOptions.PathInfos = libraryOptions + .PathInfos + .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal)) + .ToArray(); + + CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions); } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index 4a533ff93..e7bfe56f2 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -221,8 +221,28 @@ namespace MediaBrowser.Server.Implementations.Library } } - public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution) + public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken) { + if (!string.IsNullOrWhiteSpace(liveStreamId)) + { + return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false); + } + //await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + //try + //{ + // var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); + + // if (stream != null) + // { + // return stream.MediaSource; + // } + //} + //finally + //{ + // _liveStreamSemaphore.Release(); + //} + var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video }, CancellationToken.None).ConfigureAwait(false); @@ -335,7 +355,7 @@ namespace MediaBrowser.Server.Implementations.Library .ToList(); } - private readonly ConcurrentDictionary<string, LiveStreamInfo> _openStreams = new ConcurrentDictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken) @@ -347,7 +367,9 @@ namespace MediaBrowser.Server.Implementations.Library var tuple = GetProvider(request.OpenToken); var provider = tuple.Item1; - var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); + var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); + + var mediaSource = mediaSourceTuple.Item1; if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) { @@ -361,9 +383,11 @@ namespace MediaBrowser.Server.Implementations.Library Date = DateTime.UtcNow, EnableCloseTimer = enableAutoClose, Id = mediaSource.LiveStreamId, - MediaSource = mediaSource + MediaSource = mediaSource, + DirectStreamProvider = mediaSourceTuple.Item2 }; - _openStreams.AddOrUpdate(mediaSource.LiveStreamId, info, (key, i) => info); + + _openStreams[mediaSource.LiveStreamId] = info; if (enableAutoClose) { @@ -394,14 +418,14 @@ namespace MediaBrowser.Server.Implementations.Library } } - public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken) + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException("id"); } - _logger.Debug("Getting live stream {0}", id); + _logger.Debug("Getting already opened live stream {0}", id); await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -410,7 +434,7 @@ namespace MediaBrowser.Server.Implementations.Library LiveStreamInfo info; if (_openStreams.TryGetValue(id, out info)) { - return info.MediaSource; + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider); } else { @@ -423,6 +447,12 @@ namespace MediaBrowser.Server.Implementations.Library } } + public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken) + { + var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false); + return result.Item1; + } + public async Task PingLiveStream(string id, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -436,7 +466,7 @@ namespace MediaBrowser.Server.Implementations.Library } else { - _logger.Error("Failed to update MediaSource timestamp for {0}", id); + _logger.Error("Failed to ping live stream {0}", id); } } finally @@ -445,17 +475,16 @@ namespace MediaBrowser.Server.Implementations.Library } } - private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId, CancellationToken cancellationToken) + private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId) { _logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name); try { - await provider.CloseMediaSource(streamId, cancellationToken).ConfigureAwait(false); + await provider.CloseMediaSource(streamId).ConfigureAwait(false); } catch (NotImplementedException) { - } catch (Exception ex) { @@ -463,37 +492,35 @@ namespace MediaBrowser.Server.Implementations.Library } } - public async Task CloseLiveStream(string id, CancellationToken cancellationToken) + public async Task CloseLiveStream(string id) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException("id"); } - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false); try { LiveStreamInfo current; + if (_openStreams.TryGetValue(id, out current)) { + _openStreams.Remove(id); + current.Closed = true; + if (current.MediaSource.RequiresClosing) { var tuple = GetProvider(id); - await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false); + await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false); } - } - LiveStreamInfo removed; - if (_openStreams.TryRemove(id, out removed)) - { - removed.Closed = true; - } - - if (_openStreams.Count == 0) - { - StopCloseTimer(); + if (_openStreams.Count == 0) + { + StopCloseTimer(); + } } } finally @@ -523,7 +550,7 @@ namespace MediaBrowser.Server.Implementations.Library } private Timer _closeTimer; - private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(60); + private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(180); private void StartCloseTimer() { @@ -545,10 +572,20 @@ namespace MediaBrowser.Server.Implementations.Library private async void CloseTimerCallback(object state) { - var infos = _openStreams - .Values - .Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge) - .ToList(); + List<LiveStreamInfo> infos; + await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + infos = _openStreams + .Values + .Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge) + .ToList(); + } + finally + { + _liveStreamSemaphore.Release(); + } foreach (var info in infos) { @@ -556,7 +593,7 @@ namespace MediaBrowser.Server.Implementations.Library { try { - await CloseLiveStream(info.Id, CancellationToken.None).ConfigureAwait(false); + await CloseLiveStream(info.Id).ConfigureAwait(false); } catch (Exception ex) { @@ -588,12 +625,10 @@ namespace MediaBrowser.Server.Implementations.Library { foreach (var key in _openStreams.Keys.ToList()) { - var task = CloseLiveStream(key, CancellationToken.None); + var task = CloseLiveStream(key); Task.WaitAll(task); } - - _openStreams.Clear(); } } } @@ -605,6 +640,7 @@ namespace MediaBrowser.Server.Implementations.Library public string Id; public bool Closed; public MediaSourceInfo MediaSource; + public IDirectStreamProvider DirectStreamProvider; } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 1a8295800..7f35fc3ea 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -54,8 +54,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio if (!args.IsDirectory) return null; // Avoid mis-identifying top folders - if (args.Parent.IsRoot) return null; if (args.HasParent<MusicAlbum>()) return null; + if (args.Parent.IsRoot) return null; var collectionType = args.GetCollectionType(); diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index e819af06f..686105ddb 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -7,6 +7,7 @@ using System; using System.IO; using System.Linq; using CommonIO; +using MediaBrowser.Controller.Configuration; namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { @@ -18,12 +19,14 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; - public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager) + public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config) { _logger = logger; _fileSystem = fileSystem; _libraryManager = libraryManager; + _config = config; } /// <summary> @@ -48,9 +51,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { if (!args.IsDirectory) return null; - // Avoid mis-identifying top folders - if (args.Parent.IsRoot) return null; - // Don't allow nested artists if (args.HasParent<MusicArtist>() || args.HasParent<MusicAlbum>()) { @@ -67,6 +67,19 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio return null; } + if (args.ContainsFileSystemEntryByName("artist.nfo")) + { + return new MusicArtist(); + } + + if (_config.Configuration.EnableSimpleArtistDetection) + { + return null; + } + + // Avoid mis-identifying top folders + if (args.Parent.IsRoot) return null; + var directoryService = args.DirectoryService; var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs index 34237622d..ff07c5282 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs @@ -51,7 +51,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers base.SetInitialItemValues(item, args); item.IsRoot = args.Parent == null; - item.IsPhysicalRoot = args.IsPhysicalRoot; } } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index ee9533d2a..c3d5f3441 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -48,12 +48,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies string collectionType, IDirectoryService directoryService) { - if (parent != null && parent.Path != null && parent.Path.IndexOf("disney", StringComparison.OrdinalIgnoreCase) != -1) - { - var b = true; - var a = b; - } - var result = ResolveMultipleInternal(parent, files, collectionType, directoryService); if (result != null) @@ -213,26 +207,22 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies // Find movies with their own folders if (args.IsDirectory) { - var files = args.FileSystemChildren - .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) - .ToList(); - if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<MusicVideo>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + return null; } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + return null; } if (string.IsNullOrEmpty(collectionType)) { - // Owned items should just use the plain video type + // Owned items will be caught by the plain video resolver if (args.Parent == null) { - return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + return null; } if (args.HasParent<Series>()) @@ -240,11 +230,21 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return null; } - return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + { + var files = args.FileSystemChildren + .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .ToList(); + + return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType); + } } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { + var files = args.FileSystemChildren + .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .ToList(); + return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 4c6254330..c82825007 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -54,11 +54,20 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV { if (args.IsDirectory) { - if (args.HasParent<Series>()) + if (args.HasParent<Series>() || args.HasParent<Season>()) { return null; } + if (args.ContainsFileSystemEntryByName("tvshow.nfo")) + { + return new Series + { + Path = args.Path, + Name = Path.GetFileName(args.Path) + }; + } + var collectionType = args.GetCollectionType(); if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { @@ -72,22 +81,20 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV }; } } - else + else if (string.IsNullOrWhiteSpace(collectionType)) { - if (string.IsNullOrWhiteSpace(collectionType)) + if (args.Parent.IsRoot) { - if (args.Parent.IsRoot) - { - return null; - } - if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false)) + return null; + } + + if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false)) + { + return new Series { - return new Series - { - Path = args.Path, - Name = Path.GetFileName(args.Path) - }; - } + Path = args.Path, + Name = Path.GetFileName(args.Path) + }; } } } diff --git a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs index a6d6b5cb8..40cca7bab 100644 --- a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs @@ -105,6 +105,7 @@ namespace MediaBrowser.Server.Implementations.Library var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList(); excludeItemTypes.Add(typeof(Year).Name); + excludeItemTypes.Add(typeof(Folder).Name); if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase))) { diff --git a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs index 307cf4cd2..9ee65a57c 100644 --- a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs @@ -61,16 +61,7 @@ namespace MediaBrowser.Server.Implementations.Library foreach (var key in keys) { - try - { - await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error saving user data", ex); - - throw; - } + await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); } var cacheKey = GetCacheKey(userId, item.Id); @@ -107,18 +98,7 @@ namespace MediaBrowser.Server.Implementations.Library cancellationToken.ThrowIfCancellationRequested(); - try - { - await Repository.SaveAllUserData(userId, userData, cancellationToken).ConfigureAwait(false); - - } - catch (Exception ex) - { - _logger.ErrorException("Error saving user data", ex); - - throw; - } - + await Repository.SaveAllUserData(userId, userData, cancellationToken).ConfigureAwait(false); } /// <summary> @@ -289,6 +269,11 @@ namespace MediaBrowser.Server.Implementations.Library positionTicks = 0; } + if (!item.SupportsPlayedStatus) + { + positionTicks = 0; + data.Played = false; + } if (item is Audio) { positionTicks = 0; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index b21aa904b..0f8c15e71 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -47,25 +47,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Info("Copying recording stream to file {0}", targetFile); - if (mediaSource.RunTimeTicks.HasValue) - { - // The media source already has a fixed duration - // But add another stop 1 minute later just in case the recording gets stuck for any reason - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - else - { - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - - await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + // The media source if infinite so we need to handle stopping ourselves + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + + await CopyUntilCancelled(response.Content, output, cancellationToken).ConfigureAwait(false); } } _logger.Info("Recording completed to file {0}", targetFile); } + + private const int BufferSize = 81920; + public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) + { + return CopyUntilCancelled(source, target, null, cancellationToken); + } + public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false); + + onStarted = null; + + //var position = fs.Position; + //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); + + if (bytesRead == 0) + { + await Task.Delay(100).ConfigureAwait(false); + } + } + } + + private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + int bytesRead; + int totalBytesRead = 0; + + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + + totalBytesRead += bytesRead; + + if (onStarted != null) + { + onStarted(); + } + onStarted = null; + } + + return totalBytesRead; + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 8fa34109d..136a9e195 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -22,20 +22,25 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Xml; using CommonIO; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Power; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.FileOrganization; using Microsoft.Win32; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { - public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IDisposable + public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable { - private readonly IApplicationHost _appHpst; + private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; @@ -55,17 +60,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public static EmbyTV Current; - public event EventHandler DataSourceChanged { add { } remove { } } - public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged { add { } remove { } } + public event EventHandler DataSourceChanged; + public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged; private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement) + public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder) { Current = this; - _appHpst = appHost; + _appHost = appHost; _logger = logger; _httpClient = httpClient; _config = config; @@ -79,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _jsonSerializer = jsonSerializer; _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); - _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger); + _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger); _timerProvider.TimerFired += _timerProvider_TimerFired; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; @@ -140,9 +145,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV continue; } + var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray(); + + var libraryOptions = new LibraryOptions + { + PathInfos = mediaPathInfos + }; try { - _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), new LibraryOptions(), true); + _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true); } catch (Exception ex) { @@ -284,7 +295,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV status.Tuners = list; status.Status = LiveTvServiceStatus.Ok; - status.Version = _appHpst.ApplicationVersion.ToString(); + status.Version = _appHost.ApplicationVersion.ToString(); status.IsVisible = false; return status; } @@ -321,27 +332,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id)) { - _timerProvider.Delete(timer); + OnTimerOutOfDate(timer); } } } - private List<ChannelInfo> _channelCache = null; - private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken) + private void OnTimerOutOfDate(TimerInfo timer) { - if (enableCache && _channelCache != null) - { - - return _channelCache.ToList(); - } + _timerProvider.Delete(timer); + } + private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken) + { var list = new List<ChannelInfo>(); foreach (var hostInstance in _liveTvManager.TunerHosts) { try { - var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false); + var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false); list.AddRange(channels); } @@ -374,7 +383,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - _channelCache = list.ToList(); return list; } @@ -386,7 +394,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { try { - var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false); + var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false); list.AddRange(channels); } @@ -415,7 +423,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV foreach (var timer in timers) { - CancelTimerInternal(timer.Id); + CancelTimerInternal(timer.Id, true); } var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase)); @@ -426,12 +434,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult(true); } - private void CancelTimerInternal(string timerId) + private void CancelTimerInternal(string timerId, bool isSeriesCancelled) { - var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (remove != null) + var timer = _timerProvider.GetTimer(timerId); + if (timer != null) { - _timerProvider.Delete(remove); + if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled) + { + _timerProvider.Delete(timer); + } + else + { + timer.Status = RecordingStatus.Cancelled; + _timerProvider.AddOrUpdate(timer, false); + } } ActiveRecordingInfo activeRecordingInfo; @@ -443,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) { - CancelTimerInternal(timerId); + CancelTimerInternal(timerId, false); return Task.FromResult(true); } @@ -452,21 +468,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult(true); } - public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { - return CreateTimer(info, cancellationToken); + throw new NotImplementedException(); } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) { - return CreateSeriesTimer(info, cancellationToken); + throw new NotImplementedException(); } - public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken) + public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken) { - info.Id = Guid.NewGuid().ToString("N"); - _timerProvider.Add(info); - return Task.FromResult(info.Id); + var existingTimer = _timerProvider.GetAll() + .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase)); + + if (existingTimer != null) + { + if (existingTimer.Status == RecordingStatus.Cancelled || + existingTimer.Status == RecordingStatus.Completed) + { + existingTimer.Status = RecordingStatus.New; + _timerProvider.Update(existingTimer); + return Task.FromResult(existingTimer.Id); + } + else + { + throw new ArgumentException("A scheduled recording already exists for this program."); + } + } + + timer.Id = Guid.NewGuid().ToString("N"); + + ProgramInfo programInfo = null; + + if (!string.IsNullOrWhiteSpace(timer.ProgramId)) + { + programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); + } + if (programInfo == null) + { + _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); + programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); + } + + if (programInfo != null) + { + RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer); + } + + _timerProvider.Add(timer); + return Task.FromResult(timer.Id); } public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) @@ -520,6 +572,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV instance.RecordAnyChannel = info.RecordAnyChannel; instance.RecordAnyTime = info.RecordAnyTime; instance.RecordNewOnly = info.RecordNewOnly; + instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary; + instance.KeepUpTo = info.KeepUpTo; + instance.KeepUntil = info.KeepUntil; instance.StartDate = info.StartDate; _seriesTimerProvider.Update(instance); @@ -540,12 +595,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken) { - _timerProvider.Update(info); + var existingTimer = _timerProvider.GetTimer(updatedTimer.Id); + + if (existingTimer == null) + { + throw new ResourceNotFoundException(); + } + + // Only update if not currently active + ActiveRecordingInfo activeRecordingInfo; + if (!_activeRecordings.TryGetValue(updatedTimer.Id, out activeRecordingInfo)) + { + existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds; + existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds; + existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired; + existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired; + } + return Task.FromResult(true); } + private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer) + { + // Update the program info but retain the status + existingTimer.ChannelId = updatedTimer.ChannelId; + existingTimer.CommunityRating = updatedTimer.CommunityRating; + existingTimer.EndDate = updatedTimer.EndDate; + existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber; + existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle; + existingTimer.Genres = updatedTimer.Genres; + existingTimer.HomePageUrl = updatedTimer.HomePageUrl; + existingTimer.IsKids = updatedTimer.IsKids; + existingTimer.IsNews = updatedTimer.IsNews; + existingTimer.IsMovie = updatedTimer.IsMovie; + existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries; + existingTimer.IsRepeat = updatedTimer.IsRepeat; + existingTimer.IsSports = updatedTimer.IsSports; + existingTimer.Name = updatedTimer.Name; + existingTimer.OfficialRating = updatedTimer.OfficialRating; + existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate; + existingTimer.Overview = updatedTimer.Overview; + existingTimer.ProductionYear = updatedTimer.ProductionYear; + existingTimer.ProgramId = updatedTimer.ProgramId; + existingTimer.SeasonNumber = updatedTimer.SeasonNumber; + existingTimer.ShortOverview = updatedTimer.ShortOverview; + existingTimer.StartDate = updatedTimer.StartDate; + } + public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -563,12 +661,76 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) { - return new List<RecordingInfo>(); + return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList(); + } + + public string GetActiveRecordingPath(string id) + { + ActiveRecordingInfo info; + + if (_activeRecordings.TryGetValue(id, out info)) + { + return info.Path; + } + return null; + } + + private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info) + { + var timer = info.Timer; + var program = info.Program; + + var result = new RecordingInfo + { + ChannelId = timer.ChannelId, + CommunityRating = timer.CommunityRating, + DateLastUpdated = DateTime.UtcNow, + EndDate = timer.EndDate, + EpisodeTitle = timer.EpisodeTitle, + Genres = timer.Genres, + Id = "recording" + timer.Id, + IsKids = timer.IsKids, + IsMovie = timer.IsMovie, + IsNews = timer.IsNews, + IsRepeat = timer.IsRepeat, + IsSeries = timer.IsProgramSeries, + IsSports = timer.IsSports, + Name = timer.Name, + OfficialRating = timer.OfficialRating, + OriginalAirDate = timer.OriginalAirDate, + Overview = timer.Overview, + ProgramId = timer.ProgramId, + SeriesTimerId = timer.SeriesTimerId, + StartDate = timer.StartDate, + Status = RecordingStatus.InProgress, + TimerId = timer.Id + }; + + if (program != null) + { + result.Audio = program.Audio; + result.ImagePath = program.ImagePath; + result.ImageUrl = program.ImageUrl; + result.IsHD = program.IsHD; + result.IsLive = program.IsLive; + result.IsPremiere = program.IsPremiere; + result.ShowId = program.ShowId; + } + + return result; } public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken) { - return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll()); + var excludeStatues = new List<RecordingStatus> + { + RecordingStatus.Completed + }; + + var timers = _timerProvider.GetAll() + .Where(i => !excludeStatues.Contains(i.Status)); + + return Task.FromResult(timers); } public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null) @@ -581,7 +743,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0), RecordAnyChannel = true, RecordAnyTime = true, - RecordNewOnly = false, + RecordNewOnly = true, Days = new List<DayOfWeek> { @@ -601,6 +763,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV defaults.ProgramId = program.Id; } + defaults.SkipEpisodesInLibrary = true; + defaults.KeepUntil = KeepUntil.UntilDeleted; + return Task.FromResult(defaults); } @@ -717,46 +882,108 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } + private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1); + private readonly List<LiveStream> _liveStreams = new List<LiveStream>(); + public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - _logger.Info("Streaming Channel " + channelId); + var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false); - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + return result.Item1; + } - result.Item2.Release(); + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken) + { + var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false); - return result.Item1; - } - catch (Exception e) - { - _logger.ErrorException("Error getting channel stream", e); - } + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider); + } + + private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) + { + var json = _jsonSerializer.SerializeToString(mediaSource); + mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); + + mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id; + + //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing) + //{ + // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks; + // ticks = Math.Max(0, ticks); + // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture); + //} + + return mediaSource; + } + + public async Task<LiveStream> GetLiveStream(string uniqueId) + { + await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + return _liveStreams + .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase)); + } + finally + { + _liveStreamsSemaphore.Release(); } - throw new ApplicationException("Tuner not found."); } - private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + private async Task<Tuple<LiveStream, MediaSourceInfo, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) { _logger.Info("Streaming Channel " + channelId); - foreach (var hostInstance in _liveTvManager.TunerHosts) + await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + var result = _liveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase)); + + if (result != null && result.EnableStreamSharing) { - try - { - var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing); + result.SharedStreamIds.Add(openedMediaSource.Id); + _liveStreamsSemaphore.Release(); - return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2); - } - catch (Exception e) + _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount); + + return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, result.TunerHost); + } + + try + { + foreach (var hostInstance in _liveTvManager.TunerHosts) { - _logger.ErrorException("Error getting channel stream", e); + try + { + result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + + var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing); + + result.SharedStreamIds.Add(openedMediaSource.Id); + _liveStreams.Add(result); + + result.TunerHost = hostInstance; + result.OriginalStreamId = streamId; + + _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}", + streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId); + + return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, hostInstance); + } + catch (FileNotFoundException) + { + } + catch (OperationCanceledException) + { + } } } + finally + { + _liveStreamsSemaphore.Release(); + } throw new ApplicationException("Tuner not found."); } @@ -783,14 +1010,82 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken) + public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken) { - throw new NotImplementedException(); + ActiveRecordingInfo info; + + recordingId = recordingId.Replace("recording", string.Empty); + + if (_activeRecordings.TryGetValue(recordingId, out info)) + { + var stream = new MediaSourceInfo + { + Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveRecordings/" + recordingId + "/stream", + Id = recordingId, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true, + RequiresOpening = false, + RequiresClosing = false, + Protocol = Model.MediaInfo.MediaProtocol.Http, + BufferMs = 0 + }; + + var isAudio = false; + await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); + + return new List<MediaSourceInfo> + { + stream + }; + } + + throw new FileNotFoundException(); } - public Task CloseLiveStream(string id, CancellationToken cancellationToken) + public async Task CloseLiveStream(string id, CancellationToken cancellationToken) { - return Task.FromResult(0); + // Ignore the consumer id + //id = id.Substring(id.IndexOf('_') + 1); + + await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var stream = _liveStreams.FirstOrDefault(i => i.SharedStreamIds.Contains(id)); + if (stream != null) + { + stream.SharedStreamIds.Remove(id); + + _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount); + + if (stream.ConsumerCount <= 0) + { + _liveStreams.Remove(stream); + + _logger.Info("Closing live stream {0}", id); + + await stream.Close().ConfigureAwait(false); + _logger.Info("Live stream {0} closed successfully", id); + } + } + else + { + _logger.Warn("Live stream not found: {0}, unable to close", id); + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); + } + finally + { + _liveStreamsSemaphore.Release(); + } } public Task RecordLiveStream(string id, CancellationToken cancellationToken) @@ -815,14 +1110,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV if (recordingEndDate <= DateTime.UtcNow) { - _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id); + _logger.Warn("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id); + OnTimerOutOfDate(timer); return; } var activeRecordingInfo = new ActiveRecordingInfo { CancellationTokenSource = new CancellationTokenSource(), - TimerId = timer.Id + Timer = timer }; if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo)) @@ -844,12 +1140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private string GetRecordingPath(TimerInfo timer, ProgramInfo info) + private string GetRecordingPath(TimerInfo timer, out string seriesPath) { var recordPath = RecordingPath; var config = GetConfiguration(); + seriesPath = null; - if (info.IsSeries) + if (timer.IsProgramSeries) { var customRecordingPath = config.SeriesRecordingPath; var allowSubfolder = true; @@ -864,29 +1161,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV recordPath = Path.Combine(recordPath, "Series"); } - var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); - var folderNameWithYear = folderName; - if (info.ProductionYear.HasValue) - { - folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; - } + var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); - if (Directory.Exists(Path.Combine(recordPath, folderName))) - { - recordPath = Path.Combine(recordPath, folderName); - } - else - { - recordPath = Path.Combine(recordPath, folderNameWithYear); - } + // Can't use the year here in the folder name because it is the year of the episode, not the series. + recordPath = Path.Combine(recordPath, folderName); + + seriesPath = recordPath; - if (info.SeasonNumber.HasValue) + if (timer.SeasonNumber.HasValue) { - folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); recordPath = Path.Combine(recordPath, folderName); } } - else if (info.IsMovie) + else if (timer.IsMovie) { var customRecordingPath = config.MovieRecordingPath; var allowSubfolder = true; @@ -901,34 +1189,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV recordPath = Path.Combine(recordPath, "Movies"); } - var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); - if (info.ProductionYear.HasValue) + var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); + if (timer.ProductionYear.HasValue) { - folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } recordPath = Path.Combine(recordPath, folderName); } - else if (info.IsKids) + else if (timer.IsKids) { if (config.EnableRecordingSubfolders) { recordPath = Path.Combine(recordPath, "Kids"); } - var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); - if (info.ProductionYear.HasValue) + var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); + if (timer.ProductionYear.HasValue) { - folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; } recordPath = Path.Combine(recordPath, folderName); } - else if (info.IsSports) + else if (timer.IsSports) { if (config.EnableRecordingSubfolders) { recordPath = Path.Combine(recordPath, "Sports"); } - recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); } else { @@ -936,54 +1224,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { recordPath = Path.Combine(recordPath, "Other"); } - recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); } - var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; + var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts"; return Path.Combine(recordPath, recordingFileName); } - private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) + private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, + ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { if (timer == null) { throw new ArgumentNullException("timer"); } - ProgramInfo info = null; + ProgramInfo programInfo = null; - if (string.IsNullOrWhiteSpace(timer.ProgramId)) + if (!string.IsNullOrWhiteSpace(timer.ProgramId)) { - _logger.Info("Timer {0} has null programId", timer.Id); + programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); } - else - { - info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); - } - - if (info == null) + if (programInfo == null) { _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); - info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); + programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); } - if (info == null) + if (programInfo != null) { - throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); + RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer); + activeRecordingInfo.Program = programInfo; } - var recordPath = GetRecordingPath(timer, info); + string seriesPath = null; + var recordPath = GetRecordingPath(timer, out seriesPath); var recordingStatus = RecordingStatus.New; - var isResourceOpen = false; - SemaphoreSlim semaphore = null; + + string liveStreamId = null; + + OnRecordingStatusChanged(); try { - var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); - isResourceOpen = true; - semaphore = result.Item3; - var mediaStreamInfo = result.Item1; + var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false); + + var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None) + .ConfigureAwait(false); + + var mediaStreamInfo = liveStreamInfo.Item2; + liveStreamId = mediaStreamInfo.Id; // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); @@ -1000,7 +1291,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var duration = recordingEndDate - DateTime.UtcNow; - _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + _logger.Info("Beginning recording. Will record for {0} minutes.", + duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); _logger.Info("Writing file to path: " + recordPath); _logger.Info("Opening recording stream from tuner provider"); @@ -1010,20 +1302,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV timer.Status = RecordingStatus.InProgress; _timerProvider.AddOrUpdate(timer, false); - result.Item3.Release(); - isResourceOpen = false; + SaveNfo(timer, recordPath, seriesPath); + EnforceKeepUpTo(timer); }; - var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); - - // If it supports supplying duration via url - if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) - { - mediaStreamInfo.Path = pathWithDuration; - mediaStreamInfo.RunTimeTicks = duration.Ticks; - } - - await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false); + await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken) + .ConfigureAwait(false); recordingStatus = RecordingStatus.Completed; _logger.Info("Recording completed: {0}", recordPath); @@ -1038,28 +1322,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.ErrorException("Error recording to {0}", ex, recordPath); recordingStatus = RecordingStatus.Error; } - finally + + if (!string.IsNullOrWhiteSpace(liveStreamId)) { - if (isResourceOpen && semaphore != null) + try { - semaphore.Release(); + await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live stream", ex); } - - _libraryManager.UnRegisterIgnoredPath(recordPath); - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); - - ActiveRecordingInfo removed; - _activeRecordings.TryRemove(timer.Id, out removed); } - if (recordingStatus == RecordingStatus.Completed) - { - timer.Status = RecordingStatus.Completed; - _timerProvider.Delete(timer); + _libraryManager.UnRegisterIgnoredPath(recordPath); + _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); - OnSuccessfulRecording(info.IsSeries, recordPath); - } - else if (DateTime.UtcNow < timer.EndDate) + ActiveRecordingInfo removed; + _activeRecordings.TryRemove(timer.Id, out removed); + + if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate) { const int retryIntervalSeconds = 60; _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds); @@ -1068,10 +1350,123 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds); _timerProvider.AddOrUpdate(timer); } + else if (File.Exists(recordPath)) + { + timer.RecordingPath = recordPath; + timer.Status = RecordingStatus.Completed; + _timerProvider.AddOrUpdate(timer, false); + OnSuccessfulRecording(timer, recordPath); + } else { _timerProvider.Delete(timer); } + + OnRecordingStatusChanged(); + } + + private void OnRecordingStatusChanged() + { + EventHelper.FireEventIfNotNull(RecordingStatusChanged, this, new RecordingStatusChangedEventArgs + { + + }, _logger); + } + + private async void EnforceKeepUpTo(TimerInfo timer) + { + if (string.IsNullOrWhiteSpace(timer.SeriesTimerId)) + { + return; + } + + var seriesTimerId = timer.SeriesTimerId; + var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase)); + + if (seriesTimer == null || seriesTimer.KeepUpTo <= 1) + { + return; + } + + if (_disposed) + { + return; + } + + await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (_disposed) + { + return; + } + + var timersToDelete = _timerProvider.GetAll() + .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath)) + .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(i => i.EndDate) + .Where(i => File.Exists(i.RecordingPath)) + .Skip(seriesTimer.KeepUpTo - 1) + .ToList(); + + await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false); + } + finally + { + _recordingDeleteSemaphore.Release(); + } + } + + private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); + private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers) + { + foreach (var timer in timers) + { + if (_disposed) + { + return; + } + + try + { + await DeleteLibraryItemForTimer(timer).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting recording", ex); + } + } + } + + private async Task DeleteLibraryItemForTimer(TimerInfo timer) + { + var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false); + + if (libraryItem != null) + { + await _libraryManager.DeleteItem(libraryItem, new DeleteOptions + { + DeleteFileLocation = true + }); + } + else + { + try + { + File.Delete(timer.RecordingPath); + } + catch (DirectoryNotFoundException) + { + + } + catch (FileNotFoundException) + { + + } + } + + _timerProvider.Delete(timer); } private string EnsureFileUnique(string path, string timerId) @@ -1099,7 +1494,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return true; } - var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase)); + var hasRecordingAtPath = _activeRecordings + .Values + .ToList() + .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)); if (hasRecordingAtPath) { @@ -1125,30 +1523,180 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return new DirectRecorder(_logger, _httpClient, _fileSystem); } - private async void OnSuccessfulRecording(bool isSeries, string path) + private async void OnSuccessfulRecording(TimerInfo timer, string path) { - if (GetConfiguration().EnableAutoOrganize) + if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize) { - if (isSeries) + try { - try + // this is to account for the library monitor holding a lock for additional time after the change is complete. + // ideally this shouldn't be hard-coded + await Task.Delay(30000).ConfigureAwait(false); + + var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); + + var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false); + + if (result.Status == FileSortingStatus.Success) { - // this is to account for the library monitor holding a lock for additional time after the change is complete. - // ideally this shouldn't be hard-coded - await Task.Delay(30000).ConfigureAwait(false); + return; + } + } + catch (Exception ex) + { + _logger.ErrorException("Error processing new recording", ex); + } + } + } - var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); + private void SaveNfo(TimerInfo timer, string recordingPath, string seriesPath) + { + try + { + if (timer.IsProgramSeries) + { + SaveSeriesNfo(timer, recordingPath, seriesPath); + } + else if (!timer.IsMovie || timer.IsSports || timer.IsNews) + { + SaveVideoNfo(timer, recordingPath); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error saving nfo", ex); + } + } + + private void SaveSeriesNfo(TimerInfo timer, string recordingPath, string seriesPath) + { + var nfoPath = Path.Combine(seriesPath, "tvshow.nfo"); + + if (File.Exists(nfoPath)) + { + return; + } - var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false); + using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + CloseOutput = false + }; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(true); + writer.WriteStartElement("tvshow"); + + if (!string.IsNullOrWhiteSpace(timer.Name)) + { + writer.WriteElementString("title", timer.Name); } - catch (Exception ex) + + writer.WriteEndElement(); + writer.WriteEndDocument(); + } + } + } + + public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; + private void SaveVideoNfo(TimerInfo timer, string recordingPath) + { + var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); + + if (File.Exists(nfoPath)) + { + return; + } + + using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + CloseOutput = false + }; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(true); + writer.WriteStartElement("movie"); + + if (!string.IsNullOrWhiteSpace(timer.Name)) + { + writer.WriteElementString("title", timer.Name); + } + + writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat)); + + if (timer.ProductionYear.HasValue) + { + writer.WriteElementString("year", timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); + } + if (!string.IsNullOrEmpty(timer.OfficialRating)) + { + writer.WriteElementString("mpaa", timer.OfficialRating); + } + + var overview = (timer.Overview ?? string.Empty) + .StripHtml() + .Replace(""", "'"); + + writer.WriteElementString("plot", overview); + writer.WriteElementString("lockdata", true.ToString().ToLower()); + + if (timer.CommunityRating.HasValue) + { + writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (timer.IsSports) + { + AddGenre(timer.Genres, "Sports"); + } + if (timer.IsKids) + { + AddGenre(timer.Genres, "Kids"); + AddGenre(timer.Genres, "Children"); + } + if (timer.IsNews) + { + AddGenre(timer.Genres, "News"); + } + + foreach (var genre in timer.Genres) + { + writer.WriteElementString("genre", genre); + } + + if (!string.IsNullOrWhiteSpace(timer.ShortOverview)) + { + writer.WriteElementString("outline", timer.ShortOverview); + } + + if (!string.IsNullOrWhiteSpace(timer.HomePageUrl)) { - _logger.ErrorException("Error processing new recording", ex); + writer.WriteElementString("website", timer.HomePageUrl); } + + writer.WriteEndElement(); + writer.WriteEndDocument(); } } } + private void AddGenre(List<string> genres, string genre) + { + if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase)) + { + genres.Add(genre); + } + } + private ProgramInfo GetProgramInfoFromCache(string channelId, string programId) { var epgData = GetEpgDataForChannel(channelId); @@ -1168,41 +1716,101 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return _config.GetConfiguration<LiveTvOptions>("livetv"); } + private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer) + { + if (!seriesTimer.RecordAnyTime) + { + if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks) + { + return true; + } + + if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek)) + { + return true; + } + } + + if (seriesTimer.RecordNewOnly && timer.IsRepeat) + { + return true; + } + + if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer); + } + private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers) { - var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList(); - + var allTimers = GetTimersForSeries(seriesTimer, epgData) + .ToList(); + var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false); if (registration.IsValid) { - foreach (var timer in newTimers) + foreach (var timer in allTimers) { - _timerProvider.AddOrUpdate(timer); + var existingTimer = _timerProvider.GetTimer(timer.Id); + + if (existingTimer == null) + { + if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) + { + timer.Status = RecordingStatus.Cancelled; + } + _timerProvider.Add(timer); + } + else + { + // Only update if not currently active + ActiveRecordingInfo activeRecordingInfo; + if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo)) + { + UpdateExistingTimerWithNewMetadata(existingTimer, timer); + + if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) + { + existingTimer.Status = RecordingStatus.Cancelled; + } + + existingTimer.SeriesTimerId = seriesTimer.Id; + _timerProvider.Update(existingTimer); + } + } } } if (deleteInvalidTimers) { - var allTimers = GetTimersForSeries(seriesTimer, epgData, false) + var allTimerIds = allTimers .Select(i => i.Id) .ToList(); + var deleteStatuses = new List<RecordingStatus> + { + RecordingStatus.New + }; + var deletes = _timerProvider.GetAll() .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase)) - .Where(i => !allTimers.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow) + .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow) + .Where(i => deleteStatuses.Contains(i.Status)) .ToList(); foreach (var timer in deletes) { - await CancelTimerAsync(timer.Id, CancellationToken.None).ConfigureAwait(false); + CancelTimerInternal(timer.Id, false); } } } private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, - IEnumerable<ProgramInfo> allPrograms, - bool filterByCurrentRecordings) + IEnumerable<ProgramInfo> allPrograms) { if (seriesTimer == null) { @@ -1214,19 +1822,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } // Exclude programs that have already ended - allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow); + allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow); allPrograms = GetProgramsForSeries(seriesTimer, allPrograms); - if (filterByCurrentRecordings) - { - allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i)); - } - return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer)); } - private bool IsProgramAlreadyInLibrary(ProgramInfo program) + private bool IsProgramAlreadyInLibrary(TimerInfo program) { if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle)) { @@ -1281,23 +1884,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms) { - if (!seriesTimer.RecordAnyTime) - { - allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks); - - allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek)); - } - - if (seriesTimer.RecordNewOnly) - { - allPrograms = allPrograms.Where(epg => !epg.IsRepeat); - } - - if (!seriesTimer.RecordAnyChannel) - { - allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)); - } - if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) { _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series"); @@ -1341,8 +1927,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return channelIds.SelectMany(GetEpgDataForChannel).ToList(); } + private bool _disposed; public void Dispose() { + _disposed = true; foreach (var pair in _activeRecordings.ToList()) { pair.Value.CancellationTokenSource.Cancel(); @@ -1393,7 +1981,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV class ActiveRecordingInfo { public string Path { get; set; } - public string TimerId { get; set; } + public TimerInfo Timer { get; set; } + public ProgramInfo Program { get; set; } public CancellationTokenSource CancellationTokenSource { get; set; } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 75ad3de59..3e9d186e3 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -68,36 +68,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1) - { - await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); - - return; - } - - var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); - - try - { - await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) - .ConfigureAwait(false); - } - finally - { - try - { - File.Delete(tempfile); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting recording temp file", ex); - } - } - } - - private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { var durationToken = new CancellationTokenSource(duration); cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; @@ -106,59 +76,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Info("Recording completed to file {0}", targetFile); } - private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - var httpRequestOptions = new HttpRequestOptions() - { - Url = mediaSource.Path - }; - - httpRequestOptions.BufferContent = false; - - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) - { - _logger.Info("Opened recording stream from tuner provider"); - - Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); - - using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - //onStarted(); - - _logger.Info("Copying recording stream to file {0}", tempFile); - - var bufferMs = 5000; - - if (mediaSource.RunTimeTicks.HasValue) - { - // The media source already has a fixed duration - // But add another stop 1 minute later just in case the recording gets stuck for any reason - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - else - { - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs))); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - } - - var tempFileTask = response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken); - - // Give the temp file a little time to build up - await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false); - - var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, duration, onStarted, cancellationToken), cancellationToken); - - await tempFileTask.ConfigureAwait(false); - - await recordTask.ConfigureAwait(false); - } - } - - _logger.Info("Recording completed to file {0}", targetFile); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -200,7 +117,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); - process.Exited += (sender, args) => OnFfMpegProcessExited(process); + process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile); process.Start(); @@ -214,6 +131,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback StartStreamingLog(process.StandardError.BaseStream, _logFileStream); + _logger.Info("ffmpeg recording process started for {0}", _targetPath); + return _taskCompletionSource.Task; } @@ -234,16 +153,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks); - var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -re -i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + var inputModifiers = "-fflags +genpts -async 1 -vsync -1"; + var commandLineArgs = "-i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + + long startTimeTicks = 0; + //if (mediaSource.DateLiveStreamOpened.HasValue) + //{ + // var elapsed = DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value; + // elapsed -= TimeSpan.FromSeconds(10); + // if (elapsed.TotalSeconds >= 0) + // { + // startTimeTicks = elapsed.Ticks + startTimeTicks; + // } + //} if (mediaSource.ReadAtNativeFramerate) { - commandLineArgs = "-re " + commandLineArgs; + inputModifiers += " -re"; + } + + if (startTimeTicks > 0) + { + inputModifiers = "-ss " + _mediaEncoder.GetTimeParameter(startTimeTicks) + " " + inputModifiers; } commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam); - return commandLineArgs; + return inputModifiers + " " + commandLineArgs; } private string GetAudioArgs(MediaSourceInfo mediaSource) @@ -309,8 +245,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV /// <summary> /// Processes the exited. /// </summary> - /// <param name="process">The process.</param> - private void OnFfMpegProcessExited(Process process) + private void OnFfMpegProcessExited(Process process, string inputFile) { _hasExited = true; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 9485e0325..f7b4b3fde 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller.LiveTv; using System; using System.Globalization; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -12,37 +13,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds); } - public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo series) + public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo seriesTimer) { var timer = new TimerInfo(); timer.ChannelId = parent.ChannelId; - timer.Id = (series.Id + parent.Id).GetMD5().ToString("N"); + timer.Id = (seriesTimer.Id + parent.Id).GetMD5().ToString("N"); timer.StartDate = parent.StartDate; timer.EndDate = parent.EndDate; timer.ProgramId = parent.Id; - timer.PrePaddingSeconds = series.PrePaddingSeconds; - timer.PostPaddingSeconds = series.PostPaddingSeconds; - timer.IsPostPaddingRequired = series.IsPostPaddingRequired; - timer.IsPrePaddingRequired = series.IsPrePaddingRequired; - timer.Priority = series.Priority; + timer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; + timer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; + timer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; + timer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; + timer.KeepUntil = seriesTimer.KeepUntil; + timer.Priority = seriesTimer.Priority; timer.Name = parent.Name; timer.Overview = parent.Overview; - timer.SeriesTimerId = series.Id; + timer.SeriesTimerId = seriesTimer.Id; + + CopyProgramInfoToTimerInfo(parent, timer); return timer; } - public static string GetRecordingName(TimerInfo timer, ProgramInfo info) + public static void CopyProgramInfoToTimerInfo(ProgramInfo programInfo, TimerInfo timerInfo) { - if (info == null) - { - return timer.ProgramId; - } + timerInfo.SeasonNumber = programInfo.SeasonNumber; + timerInfo.EpisodeNumber = programInfo.EpisodeNumber; + timerInfo.IsMovie = programInfo.IsMovie; + timerInfo.IsKids = programInfo.IsKids; + timerInfo.IsNews = programInfo.IsNews; + timerInfo.IsSports = programInfo.IsSports; + timerInfo.ProductionYear = programInfo.ProductionYear; + timerInfo.EpisodeTitle = programInfo.EpisodeTitle; + timerInfo.OriginalAirDate = programInfo.OriginalAirDate; + timerInfo.IsProgramSeries = programInfo.IsSeries; + timerInfo.HomePageUrl = programInfo.HomePageUrl; + timerInfo.CommunityRating = programInfo.CommunityRating; + timerInfo.ShortOverview = programInfo.ShortOverview; + timerInfo.OfficialRating = programInfo.OfficialRating; + timerInfo.IsRepeat = programInfo.IsRepeat; + } + + public static string GetRecordingName(TimerInfo info) + { var name = info.Name; - if (info.IsSeries) + if (info.IsProgramSeries) { var addHyphen = true; diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 423358906..bddce0420 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -9,7 +9,6 @@ using System.Globalization; using System.Linq; using System.Threading; using CommonIO; -using MediaBrowser.Controller.Power; using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV @@ -17,15 +16,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public class TimerManager : ItemDataProvider<TimerInfo> { private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase); - private readonly IPowerManagement _powerManagement; private readonly ILogger _logger; public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired; - public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, IPowerManagement powerManagement, ILogger logger1) + public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1) : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { - _powerManagement = powerManagement; _logger = logger1; } @@ -35,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV foreach (var item in GetAll().ToList()) { - AddTimer(item); + AddOrUpdateSystemTimer(item); } } @@ -58,18 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public override void Update(TimerInfo item) { base.Update(item); - - Timer timer; - if (_timers.TryGetValue(item.Id, out timer)) - { - var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow; - timer.Change(timespan, TimeSpan.Zero); - ScheduleWake(item); - } - else - { - AddTimer(item); - } + AddOrUpdateSystemTimer(item); } public void AddOrUpdate(TimerInfo item, bool resetTimer) @@ -100,13 +86,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } base.Add(item); - AddTimer(item); - ScheduleWake(item); + AddOrUpdateSystemTimer(item); + } + + private bool ShouldStartTimer(TimerInfo item) + { + if (item.Status == RecordingStatus.Completed || + item.Status == RecordingStatus.Cancelled) + { + return false; + } + + return true; } - private void AddTimer(TimerInfo item) + private void AddOrUpdateSystemTimer(TimerInfo item) { - if (item.Status == RecordingStatus.Completed) + StopTimer(item); + + if (!ShouldStartTimer(item)) { return; } @@ -120,33 +118,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return; } - var timerLength = startDate - now; - StartTimer(item, timerLength); + var dueTime = startDate - now; + StartTimer(item, dueTime); } - private void ScheduleWake(TimerInfo info) + private void StartTimer(TimerInfo item, TimeSpan dueTime) { - var startDate = RecordingHelper.GetStartTime(info).AddMinutes(-5); - - try - { - _powerManagement.ScheduleWake(startDate); - _logger.Info("Scheduled system wake timer at {0} (UTC)", startDate); - } - catch (NotImplementedException) - { - - } - catch (Exception ex) - { - _logger.ErrorException("Error scheduling wake timer", ex); - } - } - - public void StartTimer(TimerInfo item, TimeSpan dueTime) - { - StopTimer(item); - var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); if (_timers.TryAdd(item.Id, timer)) @@ -179,5 +156,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger); } } + + public TimerInfo GetTimer(string id) + { + return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index d1d8df2e8..d3549aef5 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -11,6 +11,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Emby.XmlTv.Classes; +using Emby.XmlTv.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -115,7 +116,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings var reader = new XmlTvReader(path, GetLanguage(), null); var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken); - return results.Select(p => new ProgramInfo() + return results.Select(p => GetProgramInfo(p, info)); + } + + private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info) + { + var programInfo = new ProgramInfo { ChannelId = p.ChannelId, EndDate = GetDate(p.EndDate), @@ -141,7 +147,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null, CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null, SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null - }); + }; + + if (programInfo.IsMovie) + { + programInfo.IsSeries = false; + programInfo.EpisodeNumber = null; + programInfo.EpisodeTitle = null; + } + + return programInfo; } private DateTime GetDate(DateTime date) diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs new file mode 100644 index 000000000..336c32bae --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs @@ -0,0 +1,110 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv +{ + public class LiveStreamHelper + { + private readonly IMediaEncoder _mediaEncoder; + private readonly ILogger _logger; + + public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger) + { + _mediaEncoder = mediaEncoder; + _logger = logger; + } + + public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) + { + var originalRuntime = mediaSource.RunTimeTicks; + + var now = DateTime.UtcNow; + + var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest + { + InputPath = mediaSource.Path, + Protocol = mediaSource.Protocol, + MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video, + ExtractChapters = false, + AnalyzeDurationSections = 2 + + }, cancellationToken).ConfigureAwait(false); + + _logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + + 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; + } + } + + // This is coming up false and preventing stream copy + videoStream.IsAVC = null; + } + + // Try to estimate this + if (!mediaSource.Bitrate.HasValue) + { + var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum(); + + if (total > 0) + { + mediaSource.Bitrate = total; + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs index 683377c61..8c46b4597 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -52,6 +53,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv PostPaddingSeconds = info.PostPaddingSeconds, IsPostPaddingRequired = info.IsPostPaddingRequired, IsPrePaddingRequired = info.IsPrePaddingRequired, + KeepUntil = info.KeepUntil, ExternalChannelId = info.ChannelId, ExternalSeriesTimerId = info.SeriesTimerId, ServiceName = service.Name, @@ -71,6 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions()); dto.ProgramInfo.TimerId = dto.Id; + dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId; } @@ -100,6 +103,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv Priority = info.Priority, RecordAnyChannel = info.RecordAnyChannel, RecordAnyTime = info.RecordAnyTime, + SkipEpisodesInLibrary = info.SkipEpisodesInLibrary, + KeepUpTo = info.KeepUpTo, + KeepUntil = info.KeepUntil, RecordNewOnly = info.RecordNewOnly, ExternalChannelId = info.ChannelId, ExternalProgramId = info.ProgramId, @@ -120,6 +126,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days); + if (!string.IsNullOrWhiteSpace(info.SeriesId)) + { + var program = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + ExternalSeriesId = info.SeriesId, + Limit = 1, + ImageTypes = new ImageType[] { ImageType.Primary } + + }).FirstOrDefault(); + + if (program != null) + { + var image = program.GetImageInfo(ImageType.Primary, 0); + if (image != null) + { + try + { + dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); + dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + } + catch (Exception ex) + { + } + } + } + } + return dto; } @@ -244,6 +278,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv PostPaddingSeconds = dto.PostPaddingSeconds, IsPostPaddingRequired = dto.IsPostPaddingRequired, IsPrePaddingRequired = dto.IsPrePaddingRequired, + KeepUntil = dto.KeepUntil, Priority = dto.Priority, SeriesTimerId = dto.ExternalSeriesTimerId, ProgramId = dto.ExternalProgramId, @@ -308,6 +343,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv Priority = dto.Priority, RecordAnyChannel = dto.RecordAnyChannel, RecordAnyTime = dto.RecordAnyTime, + SkipEpisodesInLibrary = dto.SkipEpisodesInLibrary, + KeepUpTo = dto.KeepUpTo, + KeepUntil = dto.KeepUntil, RecordNewOnly = dto.RecordNewOnly, ProgramId = dto.ExternalProgramId, ChannelId = dto.ExternalChannelId, diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index b3ced55a5..bac9789b5 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -62,9 +62,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv private readonly List<ILiveTvService> _services = new List<ILiveTvService>(); - private readonly ConcurrentDictionary<string, LiveStreamData> _openStreams = - new ConcurrentDictionary<string, LiveStreamData>(); - private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1); private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>(); @@ -124,9 +121,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv foreach (var service in _services) { service.DataSourceChanged += service_DataSourceChanged; + service.RecordingStatusChanged += Service_RecordingStatusChanged; } } + private void Service_RecordingStatusChanged(object sender, RecordingStatusChangedEventArgs e) + { + _lastRecordingRefreshTime = DateTime.MinValue; + } + public List<ITunerHost> TunerHosts { get { return _tunerHosts; } @@ -151,112 +154,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); - var channels = _libraryManager.GetItemList(new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { + IsMovie = query.IsMovie, + IsNews = query.IsNews, + IsKids = query.IsKids, + IsSports = query.IsSports, + IsSeries = query.IsSeries, IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, - SortBy = new[] { ItemSortBy.SortName }, - TopParentIds = new[] { topFolder.Id.ToString("N") } + SortOrder = query.SortOrder ?? SortOrder.Ascending, + TopParentIds = new[] { topFolder.Id.ToString("N") }, + IsFavorite = query.IsFavorite, + IsLiked = query.IsLiked, + StartIndex = query.StartIndex, + Limit = query.Limit + }; - }).Cast<LiveTvChannel>(); + internalQuery.OrderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending))); - if (user != null) + if (query.EnableFavoriteSorting) { - // Avoid implicitly captured closure - var currentUser = user; - - channels = channels - .Where(i => i.IsVisible(currentUser)) - .OrderBy(i => - { - double number = 0; - - if (!string.IsNullOrEmpty(i.Number)) - { - double.TryParse(i.Number, out number); - } - - return number; - - }); - - if (query.IsFavorite.HasValue) - { - var val = query.IsFavorite.Value; - - channels = channels - .Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val); - } - - if (query.IsLiked.HasValue) - { - var val = query.IsLiked.Value; - - channels = channels - .Where(i => - { - var likes = _userDataManager.GetUserData(user, i).Likes; - - return likes.HasValue && likes.Value == val; - }); - } - - if (query.IsDisliked.HasValue) - { - var val = query.IsDisliked.Value; - - channels = channels - .Where(i => - { - var likes = _userDataManager.GetUserData(user, i).Likes; - - return likes.HasValue && likes.Value != val; - }); - } + internalQuery.OrderBy.Insert(0, new Tuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending)); } - var enableFavoriteSorting = query.EnableFavoriteSorting; - - channels = channels.OrderBy(i => + if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase))) { - if (enableFavoriteSorting) - { - var userData = _userDataManager.GetUserData(user, i); - - if (userData.IsFavorite) - { - return 0; - } - if (userData.Likes.HasValue) - { - if (!userData.Likes.Value) - { - return 3; - } - - return 1; - } - } - - return 2; - }); - - var allChannels = channels.ToList(); - IEnumerable<LiveTvChannel> allEnumerable = allChannels; - - if (query.StartIndex.HasValue) - { - allEnumerable = allEnumerable.Skip(query.StartIndex.Value); + internalQuery.OrderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending)); } - if (query.Limit.HasValue) - { - allEnumerable = allEnumerable.Take(query.Limit.Value); - } + var channelResult = _libraryManager.GetItemsResult(internalQuery); var result = new QueryResult<LiveTvChannel> { - Items = allEnumerable.ToArray(), - TotalRecordCount = allChannels.Count + Items = channelResult.Items.Cast<LiveTvChannel>().ToArray(), + TotalRecordCount = channelResult.TotalRecordCount }; return result; @@ -298,32 +229,32 @@ namespace MediaBrowser.Server.Implementations.LiveTv return result.Items.FirstOrDefault(); } - private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken) { - return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false); + var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false); + + return info.Item1; } - public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken) + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken) { return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false); } - public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken) + public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(IHasMediaSources item, CancellationToken cancellationToken) { - var item = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false); - var service = GetService(item); + var baseItem = (BaseItem)item; + var service = GetService(baseItem); - return await service.GetRecordingStreamMediaSources(id, cancellationToken).ConfigureAwait(false); + return await service.GetRecordingStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false); } - public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(string id, CancellationToken cancellationToken) + public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(IHasMediaSources item, CancellationToken cancellationToken) { - var item = GetInternalChannel(id); - var service = GetService(item); + var baseItem = (LiveTvChannel)item; + var service = GetService(baseItem); - var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false); + var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false); if (sources.Count == 0) { @@ -334,7 +265,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv foreach (var source in list) { - Normalize(source, service, item.ChannelType == ChannelType.TV); + Normalize(source, service, baseItem.ChannelType == ChannelType.TV); } return list; @@ -355,79 +286,67 @@ namespace MediaBrowser.Server.Implementations.LiveTv return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); } - private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken) + private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken) { - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase)) { mediaSourceId = null; } - try + MediaSourceInfo info; + bool isVideo; + ILiveTvService service; + IDirectStreamProvider directStreamProvider = null; + + if (isChannel) { - MediaSourceInfo info; - bool isVideo; - ILiveTvService service; + var channel = GetInternalChannel(id); + isVideo = channel.ChannelType == ChannelType.TV; + service = GetService(channel); + _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); - if (isChannel) + var supportsManagedStream = service as ISupportsDirectStreamProvider; + if (supportsManagedStream != null) { - var channel = GetInternalChannel(id); - isVideo = channel.ChannelType == ChannelType.TV; - service = GetService(channel); - _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); - info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); - info.RequiresClosing = true; - - if (info.RequiresClosing) - { - var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - - info.LiveStreamId = idPrefix + info.Id; - } + var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); + info = streamInfo.Item1; + directStreamProvider = streamInfo.Item2; } else { - var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false); - isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase); - service = GetService(recording); - - _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId); - info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false); - info.RequiresClosing = true; + info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); + } + info.RequiresClosing = true; - if (info.RequiresClosing) - { - var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; + if (info.RequiresClosing) + { + var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - info.LiveStreamId = idPrefix + info.Id; - } + info.LiveStreamId = idPrefix + info.Id; } + } + else + { + var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false); + isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase); + service = GetService(recording); - _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info)); - Normalize(info, service, isVideo); + _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId); + info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false); + info.RequiresClosing = true; - var data = new LiveStreamData + if (info.RequiresClosing) { - Info = info, - IsChannel = isChannel, - ItemId = id - }; - - _openStreams.AddOrUpdate(info.Id, data, (key, i) => data); + var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; - return info; + info.LiveStreamId = idPrefix + info.Id; + } } - catch (Exception ex) - { - _logger.ErrorException("Error getting channel stream", ex); - throw; - } - finally - { - _liveStreamSemaphore.Release(); - } + _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info)); + Normalize(info, service, isVideo); + + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider); } private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo) @@ -628,11 +547,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv return item; } - private async Task<LiveTvProgram> GetProgram(ProgramInfo info, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) + private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) { var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id); - var item = _libraryManager.GetItemById(id) as LiveTvProgram; + LiveTvProgram item = null; + allExistingPrograms.TryGetValue(id, out item); + var isNew = false; var forceUpdate = false; @@ -649,6 +570,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } + var seriesId = info.SeriesId; + if (!item.ParentId.Equals(channel.Id)) { forceUpdate = true; @@ -668,6 +591,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; + item.ExternalSeriesIdLegacy = seriesId; + + if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) + { + forceUpdate = true; + } + item.ExternalSeriesId = seriesId; + item.Genres = info.Genres; item.IsHD = info.IsHD; item.IsKids = info.IsKids; @@ -698,7 +629,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.HomePageUrl = info.HomePageUrl; item.ProductionYear = info.ProductionYear; - item.PremiereDate = info.OriginalAirDate; + + if (!info.IsSeries || info.IsRepeat) + { + item.PremiereDate = info.OriginalAirDate; + } item.IndexNumber = info.EpisodeNumber; item.ParentIndexNumber = info.SeasonNumber; @@ -725,13 +660,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + var isUpdated = false; if (isNew) { - await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); } else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) { - await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + isUpdated = true; } else { @@ -741,13 +676,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (!string.Equals(etag, item.ExternalEtag, StringComparison.OrdinalIgnoreCase)) { item.ExternalEtag = etag; - await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + isUpdated = true; } } - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem)); - - return item; + return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated); } private async Task<Guid> CreateRecordingRecord(RecordingInfo info, string serviceName, Guid parentFolderId, CancellationToken cancellationToken) @@ -811,6 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv recording.IsRepeat = info.IsRepeat; recording.IsSports = info.IsSports; recording.SeriesTimerId = info.SeriesTimerId; + recording.TimerId = info.TimerId; recording.StartDate = info.StartDate; if (!dataChanged) @@ -903,8 +837,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); - var list = new List<Tuple<BaseItemDto, string, string>>(); - list.Add(new Tuple<BaseItemDto, string, string>(dto, program.ServiceName, program.ExternalId)); + var list = new List<Tuple<BaseItemDto, string, string, string>>(); + list.Add(new Tuple<BaseItemDto, string, string, string>(dto, program.ServiceName, program.ExternalId, program.ExternalSeriesIdLegacy)); await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false); @@ -932,17 +866,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv MaxStartDate = query.MaxStartDate, ChannelIds = query.ChannelIds, IsMovie = query.IsMovie, + IsSeries = query.IsSeries, IsSports = query.IsSports, IsKids = query.IsKids, + IsNews = query.IsNews, Genres = query.Genres, StartIndex = query.StartIndex, Limit = query.Limit, SortBy = query.SortBy, SortOrder = query.SortOrder ?? SortOrder.Ascending, EnableTotalRecordCount = query.EnableTotalRecordCount, - TopParentIds = new[] { topFolder.Id.ToString("N") } + TopParentIds = new[] { topFolder.Id.ToString("N") }, + DtoOptions = options }; + if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) + { + var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); + var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); + if (seriesTimer != null) + { + internalQuery.ExternalSeriesId = seriesTimer.SeriesId; + + if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) + { + // Better to return nothing than every program in the database + return new QueryResult<BaseItemDto>(); + } + } + else + { + // Better to return nothing than every program in the database + return new QueryResult<BaseItemDto>(); + } + } + if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -970,7 +928,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv return result; } - public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, CancellationToken cancellationToken) + public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken) { var user = _userManager.GetUserById(query.UserId); @@ -980,12 +938,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, + IsNews = query.IsNews, IsMovie = query.IsMovie, + IsSeries = query.IsSeries, IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, SortBy = new[] { ItemSortBy.StartDate }, - TopParentIds = new[] { topFolder.Id.ToString("N") } + TopParentIds = new[] { topFolder.Id.ToString("N") }, + DtoOptions = options }; if (query.Limit.HasValue) @@ -1009,9 +970,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv var programList = programs.ToList(); - var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false); + var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false) || (query.IsNews ?? false) || (query.IsSeries ?? false); - programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1) + programs = programList.OrderBy(i => i.StartDate.Date) .ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount)) .ThenBy(i => i.StartDate); @@ -1035,7 +996,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<QueryResult<BaseItemDto>> GetRecommendedPrograms(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken) { - var internalResult = await GetRecommendedProgramsInternal(query, cancellationToken).ConfigureAwait(false); + var internalResult = await GetRecommendedProgramsInternal(query, options, cancellationToken).ConfigureAwait(false); var user = _userManager.GetUserById(query.UserId); @@ -1092,15 +1053,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv return score; } - private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken) + private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string, string>> programs, CancellationToken cancellationToken) { var timers = new Dictionary<string, List<TimerInfo>>(); + var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>(); foreach (var programTuple in programs) { var program = programTuple.Item1; var serviceName = programTuple.Item2; var externalProgramId = programTuple.Item3; + string externalSeriesId = programTuple.Item4; if (string.IsNullOrWhiteSpace(serviceName)) { @@ -1123,18 +1086,54 @@ namespace MediaBrowser.Server.Implementations.LiveTv } var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase)); + var foundSeriesTimer = false; if (timer != null) { - program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id) - .ToString("N"); + if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error) + { + program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id) + .ToString("N"); + + program.Status = timer.Status.ToString(); + } if (!string.IsNullOrEmpty(timer.SeriesTimerId)) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, timer.SeriesTimerId) .ToString("N"); + + foundSeriesTimer = true; } } + + if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId)) + { + continue; + } + + List<SeriesTimerInfo> seriesTimerList; + if (!seriesTimers.TryGetValue(serviceName, out seriesTimerList)) + { + try + { + var tempTimers = await GetService(serviceName).GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); + seriesTimers[serviceName] = seriesTimerList = tempTimers.ToList(); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting series timer infos", ex); + seriesTimers[serviceName] = seriesTimerList = new List<SeriesTimerInfo>(); + } + } + + var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase)); + + if (seriesTimer != null) + { + program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, seriesTimer.Id) + .ToString("N"); + } } } @@ -1267,14 +1266,96 @@ namespace MediaBrowser.Server.Implementations.LiveTv var start = DateTime.UtcNow.AddHours(-1); var end = start.AddDays(guideDays); + var isMovie = false; + var isSports = false; + var isNews = false; + var isKids = false; + var iSSeries = false; + var channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false); + var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery + { + + IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + ChannelIds = new string[] { currentChannel.Id.ToString("N") } + + }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); + + var newPrograms = new List<LiveTvProgram>(); + var updatedPrograms = new List<LiveTvProgram>(); + foreach (var program in channelPrograms) { - var programItem = await GetProgram(program, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false); + var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken); + var programItem = programTuple.Item1; + + if (programTuple.Item2) + { + newPrograms.Add(programItem); + } + else if (programTuple.Item3) + { + updatedPrograms.Add(programItem); + } programs.Add(programItem.Id); + + if (program.IsMovie) + { + isMovie = true; + } + + if (program.IsSeries) + { + iSSeries = true; + } + + if (program.IsSports) + { + isSports = true; + } + + if (program.IsNews) + { + isNews = true; + } + + if (program.IsKids) + { + isKids = true; + } + } + + _logger.Debug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count); + + if (newPrograms.Count > 0) + { + await _libraryManager.CreateItems(newPrograms, cancellationToken).ConfigureAwait(false); } + + // TODO: Do this in bulk + foreach (var program in updatedPrograms) + { + await _libraryManager.UpdateItem(program, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } + + foreach (var program in newPrograms) + { + _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem)); + } + foreach (var program in updatedPrograms) + { + _providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem)); + } + + currentChannel.IsMovie = isMovie; + currentChannel.IsNews = isNews; + currentChannel.IsSports = isSports; + currentChannel.IsKids = isKids; + currentChannel.IsSeries = iSSeries; + + await currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -1361,7 +1442,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv private DateTime _lastRecordingRefreshTime; private async Task RefreshRecordings(CancellationToken cancellationToken) { - const int cacheMinutes = 5; + const int cacheMinutes = 3; if ((DateTime.UtcNow - _lastRecordingRefreshTime).TotalMinutes < cacheMinutes) { @@ -1409,9 +1490,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } - private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user) + private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user) { - if (user == null || (query.IsInProgress ?? false)) + if (user == null) + { + return new QueryResult<BaseItem>(); + } + + if ((query.IsInProgress ?? false)) { return new QueryResult<BaseItem>(); } @@ -1485,7 +1571,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv EnableTotalRecordCount = query.EnableTotalRecordCount, IncludeItemTypes = includeItemTypes.ToArray(), ExcludeItemTypes = excludeItemTypes.ToArray(), - Genres = genres.ToArray() + Genres = genres.ToArray(), + DtoOptions = dtoOptions }); } @@ -1556,9 +1643,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv return new QueryResult<BaseItem>(); } - if (_services.Count == 1) + if (_services.Count == 1 && !(query.IsInProgress ?? false)) { - return GetEmbyRecordings(query, user); + return GetEmbyRecordings(query, new DtoOptions(), user); } await RefreshRecordings(cancellationToken).ConfigureAwait(false); @@ -1609,6 +1696,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv recordings = recordings.Where(i => i.IsMovie == val); } + if (query.IsNews.HasValue) + { + var val = query.IsNews.Value; + recordings = recordings.Where(i => i.IsNews == val); + } + if (query.IsSeries.HasValue) { var val = query.IsSeries.Value; @@ -1659,7 +1752,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, List<ItemFields> fields, User user = null) { - var recordingTuples = new List<Tuple<BaseItemDto, string, string>>(); + var recordingTuples = new List<Tuple<BaseItemDto, string, string, string>>(); foreach (var tuple in tuples) { @@ -1727,7 +1820,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.ServiceName = serviceName; } - recordingTuples.Add(new Tuple<BaseItemDto, string, string>(dto, serviceName, program.ExternalId)); + recordingTuples.Add(new Tuple<BaseItemDto, string, string, string>(dto, serviceName, program.ExternalId, program.ExternalSeriesIdLegacy)); } await AddRecordingInfo(recordingTuples, CancellationToken.None).ConfigureAwait(false); @@ -1746,6 +1839,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv ? null : _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N"); + dto.TimerId = string.IsNullOrEmpty(info.TimerId) + ? null + : _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N"); + dto.StartDate = info.StartDate; dto.RecordingStatus = info.Status; dto.IsRepeat = info.IsRepeat; @@ -1842,6 +1939,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + if (query.IsScheduled.HasValue) + { + if (query.IsScheduled.Value) + { + timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); + } + else + { + timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New)); + } + } + if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); @@ -2002,6 +2111,56 @@ namespace MediaBrowser.Server.Implementations.LiveTv return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); } + private async Task<QueryResult<SeriesTimerInfo>> GetSeriesTimersInternal(SeriesTimerQuery query, CancellationToken cancellationToken) + { + var tasks = _services.Select(async i => + { + try + { + var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); + return recs.Select(r => + { + r.ServiceName = i.Name; + return new Tuple<SeriesTimerInfo, ILiveTvService>(r, i); + }); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting recordings", ex); + return new List<Tuple<SeriesTimerInfo, ILiveTvService>>(); + } + }); + var results = await Task.WhenAll(tasks).ConfigureAwait(false); + var timers = results.SelectMany(i => i.ToList()); + + if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) + { + timers = query.SortOrder == SortOrder.Descending ? + timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) : + timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name); + } + else + { + timers = query.SortOrder == SortOrder.Descending ? + timers.OrderByStringDescending(i => i.Item1.Name) : + timers.OrderByString(i => i.Item1.Name); + } + + var returnArray = timers + .Select(i => + { + return i.Item1; + + }) + .ToArray(); + + return new QueryResult<SeriesTimerInfo> + { + Items = returnArray, + TotalRecordCount = returnArray.Length + }; + } + public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) { var tasks = _services.Select(async i => @@ -2146,6 +2305,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); + info.RecordAnyChannel = true; + info.RecordAnyTime = true; + info.Days = new List<DayOfWeek> + { + DayOfWeek.Sunday, + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday + }; + info.Id = null; return new Tuple<SeriesTimerInfo, ILiveTvService>(info, service); @@ -2399,47 +2571,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } - class LiveStreamData + public async Task CloseLiveStream(string id) { - internal MediaSourceInfo Info; - internal string ItemId; - internal bool IsChannel; - } + var parts = id.Split(new[] { '_' }, 2); - public async Task CloseLiveStream(string id, CancellationToken cancellationToken) - { - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase)); - try + if (service == null) { - var parts = id.Split(new[] { '_' }, 2); - - var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase)); - - if (service == null) - { - throw new ArgumentException("Service not found."); - } - - id = parts[1]; - - LiveStreamData data; - _openStreams.TryRemove(id, out data); + throw new ArgumentException("Service not found."); + } - _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id); + id = parts[1]; - await service.CloseLiveStream(id, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error closing live stream", ex); + _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id); - throw; - } - finally - { - _liveStreamSemaphore.Release(); - } + await service.CloseLiveStream(id, CancellationToken.None).ConfigureAwait(false); } public GuideInfo GetGuideInfo() @@ -2462,7 +2609,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv Dispose(true); } - private readonly object _disposeLock = new object(); private bool _isDisposed = false; /// <summary> /// Releases unmanaged and - optionally - managed resources. @@ -2473,18 +2619,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (dispose) { _isDisposed = true; - - lock (_disposeLock) - { - foreach (var stream in _openStreams.Values.ToList()) - { - var task = CloseLiveStream(stream.Info.Id, CancellationToken.None); - - Task.WaitAll(task); - } - - _openStreams.Clear(); - } } } @@ -2620,7 +2754,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) { - info = (TunerHostInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(TunerHostInfo)); + info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info)); var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -2661,7 +2795,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) { - info = (ListingsProviderInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(ListingsProviderInfo)); + info = _jsonSerializer.DeserializeFromString< ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -2803,7 +2937,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv feature = "embytvseriesrecordings"; } - if (string.Equals(feature, "dvr", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase)) { var config = GetConfiguration(); if (config.TunerHosts.Count(i => i.IsEnabled) > 0 && diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index cdba1873e..a62796036 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -9,9 +9,11 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Dlna; namespace MediaBrowser.Server.Implementations.LiveTv { @@ -63,12 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv { if (item is ILiveTvRecording) { - sources = await _liveTvManager.GetRecordingMediaSources(item.Id.ToString("N"), cancellationToken) + sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken) .ConfigureAwait(false); } else { - sources = await _liveTvManager.GetChannelMediaSources(item.Id.ToString("N"), cancellationToken) + sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) .ConfigureAwait(false); } } @@ -116,17 +118,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv return list; } - public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken) + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken) { - MediaSourceInfo stream; + MediaSourceInfo stream = null; const bool isAudio = false; var keys = openToken.Split(new[] { StreamIdDelimeter }, 3); var mediaSourceId = keys.Length >= 3 ? keys[2] : null; + IDirectStreamProvider directStreamProvider = null; if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase)) { - stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false); + var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false); + stream = info.Item1; + directStreamProvider = info.Item2; } else { @@ -135,14 +140,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv try { - await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false); + if (stream.MediaStreams.Any(i => i.Index != -1)) + { + await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false); + } + else + { + await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false); + } } catch (Exception ex) { _logger.ErrorException("Error probing live tv stream", ex); } - return stream; + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider); } private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) @@ -204,9 +216,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } - public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken) + public Task CloseMediaSource(string liveStreamId) { - return _liveTvManager.CloseLiveStream(liveStreamId, cancellationToken); + return _liveTvManager.CloseLiveStream(liveStreamId); } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 9bb5b4fd7..0fe74798f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -6,9 +6,11 @@ using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Serialization; @@ -17,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { public abstract class BaseTunerHost { - protected readonly IConfigurationManager Config; + protected readonly IServerConfigurationManager Config; protected readonly ILogger Logger; protected IJsonSerializer JsonSerializer; protected readonly IMediaEncoder MediaEncoder; @@ -25,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase); - protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) + protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) { Config = config; Logger = logger; @@ -71,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts .ToList(); } - public async Task<IEnumerable<ChannelInfo>> GetChannels(CancellationToken cancellationToken) + public async Task<IEnumerable<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken) { var list = new List<ChannelInfo>(); @@ -81,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { try { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false); var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList(); list.AddRange(newChannels); @@ -124,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool"); - - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool"); - try { // Check to make sure the tuner is available @@ -155,89 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { Logger.Error("Error opening tuner", ex); } - finally - { - resourcePool.Release(); - } } } return new List<MediaSourceInfo>(); } - protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); + protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); - public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) + public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) { - if (IsValidChannelId(channelId)) + if (!IsValidChannelId(channelId)) { - var hosts = GetTunerHosts(); - - var hostsWithChannel = new List<TunerHostInfo>(); + throw new FileNotFoundException(); + } - foreach (var host in hosts) - { - if (string.IsNullOrWhiteSpace(streamId)) - { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); + var hosts = GetTunerHosts(); - if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) - { - hostsWithChannel.Add(host); - } - } - catch (Exception ex) - { - Logger.Error("Error getting channels", ex); - } - } - else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) - { - hostsWithChannel = new List<TunerHostInfo> { host }; - streamId = streamId.Substring(host.Id.Length); - break; - } - } + var hostsWithChannel = new List<TunerHostInfo>(); - foreach (var host in hostsWithChannel) + foreach (var host in hosts) + { + if (string.IsNullOrWhiteSpace(streamId)) { - var resourcePool = GetLock(host.Url); - Logger.Debug("GetChannelStream - Waiting on tuner resource pool"); - await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - Logger.Debug("GetChannelStream - Unlocked resource pool"); try { - // 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) - { - if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) - { - Logger.Error("Tuner is not currently available"); - resourcePool.Release(); - continue; - } - } - - var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); - if (EnableMediaProbing) + if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase))) { - await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + hostsWithChannel.Add(host); } - - return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool); } catch (Exception ex) { - Logger.Error("Error opening tuner", ex); - - resourcePool.Release(); + Logger.Error("Error getting channels", ex); } } + else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase)) + { + hostsWithChannel = new List<TunerHostInfo> { host }; + streamId = streamId.Substring(host.Id.Length); + break; + } + } + + foreach (var host in hostsWithChannel) + { + try + { + var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); + await liveStream.Open(cancellationToken).ConfigureAwait(false); + return liveStream; + } + catch (Exception ex) + { + Logger.Error("Error opening tuner", ex); + } } throw new LiveTvConflictException(); @@ -263,117 +233,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected abstract Task<bool> IsAvailableInternal(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 - { - await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); - - // Leave the resource locked. it will be released upstream - } - catch (Exception) - { - // Release the resource if there's some kind of failure. - resourcePool.Release(); - - throw; - } - } - - 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); protected LiveTvOptions GetConfiguration() diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs index 9ba1c60cc..cd168ba58 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs @@ -10,6 +10,7 @@ using System; using System.Linq; using System.Threading; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -39,13 +40,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { string server = null; - if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) + var info = e.Argument; + + if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) { string location; - if (e.Headers.TryGetValue("Location", out location)) + if (info.Headers.TryGetValue("Location", out location)) { //_logger.Debug("HdHomerun found at {0}", location); @@ -85,7 +88,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = string.Format("{0}/discover.json", url), - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false })) { var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index fd4775938..97d52836d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -14,7 +14,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; @@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; - public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient) + public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; + _fileSystem = fileSystem; + _appHost = appHost; } public string Name @@ -60,20 +67,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return id; } - public string ApplyDuration(string streamPath, TimeSpan duration) - { - streamPath += streamPath.IndexOf('?') == -1 ? "?" : "&"; - streamPath += "duration=" + Convert.ToInt32(duration.TotalSeconds).ToString(CultureInfo.InvariantCulture); - - return streamPath; - } - private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) { var options = new HttpRequestOptions { Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)), - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + BufferContent = false }; using (var stream = await _httpClient.Get(options)) { @@ -105,8 +105,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } + private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken) { + lock (_modelCache) + { + DiscoverResponse response; + if (_modelCache.TryGetValue(info.Url, out response)) + { + return response.ModelNumber; + } + } + try { using (var stream = await _httpClient.Get(new HttpRequestOptions() @@ -115,11 +125,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun CancellationToken = cancellationToken, CacheLength = TimeSpan.FromDays(1), CacheMode = CacheMode.Unconditional, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), + BufferContent = false })) { var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + lock (_modelCache) + { + _modelCache[info.Id] = response; + } + return response.ModelNumber; } } @@ -127,8 +143,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) { + var defaultValue = "HDHR"; // HDHR4 doesn't have this api - return "HDHR"; + lock (_modelCache) + { + _modelCache[info.Id] = new DiscoverResponse + { + ModelNumber = defaultValue + }; + } + return defaultValue; } throw; @@ -143,7 +167,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)), CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), + BufferContent = false })) { var tuners = new List<LiveTvTunerInfo>(); @@ -319,18 +344,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun videoBitrate = 1000000; } - if (string.IsNullOrWhiteSpace(videoCodec)) + var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); + var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase)); + if (channel != null) { - var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); - var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase)); - if (channel != null) + if (string.IsNullOrWhiteSpace(videoCodec)) { videoCodec = channel.VideoCodec; - audioCodec = channel.AudioCodec; + } + audioCodec = channel.AudioCodec; + if (!videoBitrate.HasValue) + { videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000; - audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000; } + audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000; } // normalize @@ -352,6 +380,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun url += "?transcode=" + profile; } + var id = profile; + if (string.IsNullOrWhiteSpace(id)) + { + id = "native"; + } + id += "_" + url.GetMD5().ToString("N"); + var mediaSource = new MediaSourceInfo { Path = url, @@ -380,14 +415,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun BitRate = audioBitrate } }, - RequiresOpening = false, + RequiresOpening = true, RequiresClosing = false, BufferMs = 0, Container = "ts", - Id = profile, - SupportsDirectPlay = true, - SupportsDirectStream = false, - SupportsTranscoding = true + Id = id, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true }; return mediaSource; @@ -417,18 +453,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { - string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); - model = model ?? string.Empty; - - if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) + if (info.AllowHWTranscoding) { - list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false)); + string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); + model = model ?? string.Empty; - list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false)); + if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) + { + list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false)); + + list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false)); + list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false)); + list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false)); + list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false)); + list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false)); + } } } catch @@ -449,9 +488,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { - Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); + var profile = streamId.Split('_')[0]; + + Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile); if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) { @@ -459,7 +500,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); + var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false); + + var liveStream = new HdHomerunLiveStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + liveStream.EnableStreamSharing = true; + return liveStream; } public async Task Validate(TunerHostInfo info) @@ -469,13 +514,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return; } + lock (_modelCache) + { + _modelCache.Clear(); + } + try { // Test it by pulling down the lineup using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None, + BufferContent = false })) { var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs new file mode 100644 index 000000000..60222415c --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Common.Extensions; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun +{ + public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IServerApplicationHost _appHost; + + private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource(); + private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>(); + private readonly MulticastStream _multicastStream; + + + public HdHomerunLiveStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) + : base(mediaSource) + { + _fileSystem = fileSystem; + _httpClient = httpClient; + _logger = logger; + _appPaths = appPaths; + _appHost = appHost; + OriginalStreamId = originalStreamId; + _multicastStream = new MulticastStream(_logger); + } + + protected override async Task OpenInternal(CancellationToken openCancellationToken) + { + _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + var mediaSource = OriginalMediaSource; + + var url = mediaSource.Path; + + _logger.Info("Opening HDHR Live stream from {0}", url); + + var taskCompletionSource = new TaskCompletionSource<bool>(); + + StartStreaming(url, taskCompletionSource, _liveStreamCancellationTokenSource.Token); + + //OpenedMediaSource.Protocol = MediaProtocol.File; + //OpenedMediaSource.Path = tempFile; + //OpenedMediaSource.ReadAtNativeFramerate = true; + + OpenedMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + OpenedMediaSource.Protocol = MediaProtocol.Http; + OpenedMediaSource.SupportsDirectPlay = false; + OpenedMediaSource.SupportsDirectStream = true; + OpenedMediaSource.SupportsTranscoding = true; + + await taskCompletionSource.Task.ConfigureAwait(false); + + //await Task.Delay(5000).ConfigureAwait(false); + } + + public override Task Close() + { + _logger.Info("Closing HDHR live stream"); + _liveStreamCancellationTokenSource.Cancel(); + + return _liveStreamTaskCompletionSource.Task; + } + + private async Task StartStreaming(string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + { + await Task.Run(async () => + { + var isFirstAttempt = true; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + using (var response = await _httpClient.SendAsync(new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = false + + }, "GET").ConfigureAwait(false)) + { + _logger.Info("Opened HDHR stream from {0}", url); + + if (!cancellationToken.IsCancellationRequested) + { + _logger.Info("Beginning multicastStream.CopyUntilCancelled"); + + Action onStarted = null; + if (isFirstAttempt) + { + onStarted = () => openTaskCompletionSource.TrySetResult(true); + } + + await _multicastStream.CopyUntilCancelled(response.Content, onStarted, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (isFirstAttempt) + { + _logger.ErrorException("Error opening live stream:", ex); + openTaskCompletionSource.TrySetException(ex); + break; + } + + _logger.ErrorException("Error copying live stream, will reopen", ex); + } + + isFirstAttempt = false; + } + + _liveStreamTaskCompletionSource.TrySetResult(true); + + }).ConfigureAwait(false); + } + + public Task CopyToAsync(Stream stream, CancellationToken cancellationToken) + { + return _multicastStream.CopyToAsync(stream); + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 5c508aacd..b03feefe4 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -13,8 +13,10 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { @@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return Task.FromResult(list); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + return liveStream; } public async Task Validate(TunerHostInfo info) @@ -136,7 +139,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts RequiresOpening = false, RequiresClosing = false, - ReadAtNativeFramerate = false + ReadAtNativeFramerate = false, + + Id = channel.Path.GetMD5().ToString("N"), + IsInfiniteStream = true }; return new List<MediaSourceInfo> { mediaSource }; @@ -148,10 +154,5 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts { return Task.FromResult(true); } - - public string ApplyDuration(string streamPath, TimeSpan duration) - { - return streamPath; - } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs new file mode 100644 index 000000000..8ff3fd6c1 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts +{ + public class MulticastStream + { + private readonly List<QueueStream> _outputStreams = new List<QueueStream>(); + private const int BufferSize = 81920; + private CancellationToken _cancellationToken; + private readonly ILogger _logger; + + public MulticastStream(ILogger logger) + { + _logger = logger; + } + + public async Task CopyUntilCancelled(Stream source, Action onStarted, CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + + while (!cancellationToken.IsCancellationRequested) + { + byte[] buffer = new byte[BufferSize]; + + var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + + if (bytesRead > 0) + { + byte[] copy = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead); + + List<QueueStream> streams = null; + + lock (_outputStreams) + { + streams = _outputStreams.ToList(); + } + + foreach (var stream in streams) + { + stream.Queue(copy); + } + + if (onStarted != null) + { + var onStartedCopy = onStarted; + onStarted = null; + Task.Run(onStartedCopy); + } + } + + else + { + await Task.Delay(100).ConfigureAwait(false); + } + } + } + + public Task CopyToAsync(Stream stream) + { + var result = new QueueStream(stream, _logger) + { + OnFinished = OnFinished + }; + + lock (_outputStreams) + { + _outputStreams.Add(result); + } + + result.Start(_cancellationToken); + + return result.TaskCompletion.Task; + } + + public void RemoveOutputStream(QueueStream stream) + { + lock (_outputStreams) + { + _outputStreams.Remove(stream); + } + } + + private void OnFinished(QueueStream queueStream) + { + RemoveOutputStream(queueStream); + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs new file mode 100644 index 000000000..c1566b900 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts +{ + public class QueueStream + { + private readonly Stream _outputStream; + private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>(); + private CancellationToken _cancellationToken; + public TaskCompletionSource<bool> TaskCompletion { get; private set; } + + public Action<QueueStream> OnFinished { get; set; } + private readonly ILogger _logger; + + public QueueStream(Stream outputStream, ILogger logger) + { + _outputStream = outputStream; + _logger = logger; + TaskCompletion = new TaskCompletionSource<bool>(); + } + + public void Queue(byte[] bytes) + { + _queue.Enqueue(bytes); + } + + public void Start(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + Task.Run(() => StartInternal()); + } + + private byte[] Dequeue() + { + byte[] bytes; + if (_queue.TryDequeue(out bytes)) + { + return bytes; + } + + return null; + } + + private async Task StartInternal() + { + var cancellationToken = _cancellationToken; + + try + { + while (!cancellationToken.IsCancellationRequested) + { + var bytes = Dequeue(); + if (bytes != null) + { + await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + } + else + { + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + } + + TaskCompletion.TrySetResult(true); + _logger.Debug("QueueStream complete"); + } + catch (OperationCanceledException) + { + _logger.Debug("QueueStream cancelled"); + TaskCompletion.TrySetCanceled(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in QueueStream", ex); + TaskCompletion.TrySetException(ex); + } + finally + { + if (OnFinished != null) + { + OnFinished(this); + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index cb0e573da..a0b8ef5f7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -14,6 +14,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Extensions; using System.Xml.Linq; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -50,18 +51,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; } - void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { + var info = e.Argument; + string st = null; string nt = null; - e.Headers.TryGetValue("ST", out st); - e.Headers.TryGetValue("NT", out nt); + info.Headers.TryGetValue("ST", out st); + info.Headers.TryGetValue("NT", out nt); if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) || string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase)) { string location; - if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) + if (info.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location)) { _logger.Debug("SAT IP found at {0}", location); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index b1e349a86..81deb2995 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -8,6 +8,7 @@ using CommonIO; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; @@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.LiveTv.EmbyTV; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp private readonly IFileSystem _fileSystem; private readonly IHttpClient _httpClient; - public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) + public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) : base(config, logger, jsonSerializer, mediaEncoder) { _fileSystem = fileSystem; @@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return new List<MediaSourceInfo>(); } - protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + protected override async Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) { var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); - return sources.First(); + var liveStream = new LiveStream(sources.First()); + + return liveStream; } protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json index 5e2f98c09..bc0dc236d 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json @@ -47,7 +47,6 @@ "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": "Server restart required", diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt index 723807509..ad1f18619 100644 --- a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt +++ b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt @@ -1,5 +1,10 @@ DE-0,1 +FSK-0,1 DE-6,5 +FSK-6,5 DE-12,7 +FSK-12,7 DE-16,8 +FSK-16,8 DE-18,9 +FSK-18,9
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 8850f3d35..f01a107df 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> @@ -11,9 +11,10 @@ <AssemblyName>MediaBrowser.Server.Implementations</AssemblyName> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <ReleaseVersion> </ReleaseVersion> + <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -69,12 +70,12 @@ <Reference Include="ServiceStack.Api.Swagger"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath> </Reference> - <Reference Include="SimpleInjector, Version=3.2.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> - <HintPath>..\packages\SimpleInjector.3.2.0\lib\net45\SimpleInjector.dll</HintPath> + <Reference Include="SimpleInjector, Version=3.2.2.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> + <HintPath>..\packages\SimpleInjector.3.2.2\lib\net45\SimpleInjector.dll</HintPath> <Private>True</Private> </Reference> - <Reference Include="SocketHttpListener, Version=1.0.6063.4624, Culture=neutral, processorArchitecture=MSIL"> - <HintPath>..\packages\SocketHttpListener.1.0.0.39\lib\net45\SocketHttpListener.dll</HintPath> + <Reference Include="SocketHttpListener, Version=1.0.6109.26162, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\SocketHttpListener.1.0.0.40\lib\net45\SocketHttpListener.dll</HintPath> <Private>True</Private> </Reference> <Reference Include="System" /> @@ -105,9 +106,6 @@ <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> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -164,7 +162,6 @@ <Compile Include="HttpServer\HttpListenerHost.cs" /> <Compile Include="HttpServer\HttpResultFactory.cs" /> <Compile Include="HttpServer\LoggerUtils.cs" /> - <Compile Include="HttpServer\NativeWebSocket.cs" /> <Compile Include="HttpServer\RangeRequestWriter.cs" /> <Compile Include="HttpServer\ResponseFilter.cs" /> <Compile Include="HttpServer\Security\AuthService.cs" /> @@ -237,6 +234,7 @@ <Compile Include="LiveTv\EmbyTV\TimerManager.cs" /> <Compile Include="LiveTv\Listings\SchedulesDirect.cs" /> <Compile Include="LiveTv\Listings\XmlTvListingsProvider.cs" /> + <Compile Include="LiveTv\LiveStreamHelper.cs" /> <Compile Include="LiveTv\LiveTvConfigurationFactory.cs" /> <Compile Include="LiveTv\LiveTvDtoService.cs" /> <Compile Include="LiveTv\LiveTvManager.cs" /> @@ -244,11 +242,14 @@ <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" /> + <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" /> <Compile Include="LiveTv\TunerHosts\M3uParser.cs" /> <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" /> <Compile Include="LiveTv\ProgramImageProvider.cs" /> <Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> + <Compile Include="LiveTv\TunerHosts\MulticastStream.cs" /> + <Compile Include="LiveTv\TunerHosts\QueueStream.cs" /> <Compile Include="LiveTv\TunerHosts\SatIp\ChannelScan.cs" /> <Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\ReportBlock.cs" /> <Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\RtcpAppPacket.cs" /> @@ -390,6 +391,10 @@ <Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project> <Name>MediaBrowser.Model</Name> </ProjectReference> + <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj"> + <Project>{d7453b88-2266-4805-b39b-2b5a2a33e1ba}</Project> + <Name>Mono.Nat</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <EmbeddedResource Include="Localization\Ratings\us.txt" /> diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index 7d0841fa6..21e847c68 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -61,7 +61,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder } var libraryOptions = _libraryManager.GetLibraryOptions(video); - if (libraryOptions != null && libraryOptions.SchemaVersion >= 2) + if (libraryOptions != null) { if (!libraryOptions.EnableChapterImageExtraction) { @@ -70,29 +70,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder } else { - var options = _chapterManager.GetConfiguration(); - - if (video is Movie) - { - if (!options.EnableMovieChapterImageExtraction) - { - return false; - } - } - else if (video is Episode) - { - if (!options.EnableEpisodeChapterImageExtraction) - { - return false; - } - } - else - { - if (!options.EnableOtherVideoChapterImageExtraction) - { - return false; - } - } + return false; } // Can't extract images if there are no video streams @@ -160,7 +138,9 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - var tempFile = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + var container = video.Container; + + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); File.Copy(tempFile, path, true); try diff --git a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs index 969541fbc..c15af6f70 100644 --- a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs @@ -81,7 +81,8 @@ namespace MediaBrowser.Server.Implementations.News { Url = "http://emby.media/community/index.php?/blog/rss/1-media-browser-developers-blog", 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" + UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.42 Safari/537.36", + BufferContent = false }; using (var stream = await _httpClient.Get(requestOptions).ConfigureAwait(false)) diff --git a/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs b/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs index 98d3672fa..8ca667739 100644 --- a/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs +++ b/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs @@ -91,13 +91,6 @@ namespace MediaBrowser.Server.Implementations.Notifications new NotificationTypeInfo { - Type = NotificationType.NewLibraryContentMultiple.ToString(), - DefaultTitle = "{ItemCount} new items have been added to your media library.", - Variables = new List<string>{"ItemCount"} - }, - - new NotificationTypeInfo - { Type = NotificationType.AudioPlayback.ToString(), DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.", Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"} diff --git a/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs index a16d23700..028465354 100644 --- a/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs +++ b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs @@ -4,6 +4,7 @@ using MediaBrowser.Model.Serialization; using System; using System.Data; using System.IO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Persistence { @@ -51,18 +52,16 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// Gets a stream from a DataReader at a given ordinal /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="ordinal">The ordinal.</param> /// <returns>Stream.</returns> /// <exception cref="System.ArgumentNullException">reader</exception> - public static Stream GetMemoryStream(this IDataReader reader, int ordinal) + public static Stream GetMemoryStream(this IDataReader reader, int ordinal, IMemoryStreamProvider streamProvider) { if (reader == null) { throw new ArgumentNullException("reader"); } - var memoryStream = new MemoryStream(); + var memoryStream = streamProvider.CreateNew(); var num = 0L; var array = new byte[4096]; long bytes; @@ -132,18 +131,16 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// Serializes to bytes. /// </summary> - /// <param name="json">The json.</param> - /// <param name="obj">The obj.</param> /// <returns>System.Byte[][].</returns> /// <exception cref="System.ArgumentNullException">obj</exception> - public static byte[] SerializeToBytes(this IJsonSerializer json, object obj) + public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamProvider streamProvider) { if (obj == null) { throw new ArgumentNullException("obj"); } - using (var stream = new MemoryStream()) + using (var stream = streamProvider.CreateNew()) { json.SerializeToStream(obj, stream); return stream.ToArray(); diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs index 40970dbe4..1726a77a6 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs @@ -10,6 +10,7 @@ using System.Data; using System.IO; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Persistence { @@ -18,10 +19,13 @@ namespace MediaBrowser.Server.Implementations.Persistence /// </summary> public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository { - public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector) + private readonly IMemoryStreamProvider _memoryStreamProvider; + + public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector, IMemoryStreamProvider memoryStreamProvider) : base(logManager, dbConnector) { _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); } @@ -82,7 +86,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - var serialized = _jsonSerializer.SerializeToBytes(displayPreferences); + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); using (var connection = await CreateConnection().ConfigureAwait(false)) { @@ -166,7 +170,7 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var displayPreference in displayPreferences) { - var serialized = _jsonSerializer.SerializeToBytes(displayPreference); + var serialized = _jsonSerializer.SerializeToBytes(displayPreference, _memoryStreamProvider); using (var cmd = connection.CreateCommand()) { @@ -246,7 +250,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (reader.Read()) { - using (var stream = reader.GetMemoryStream(0)) + using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) { return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); } @@ -283,7 +287,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { while (reader.Read()) { - using (var stream = reader.GetMemoryStream(0)) + using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider)) { list.Add(_jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream)); } diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index 5ece3dd82..2235bfe0d 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -19,11 +19,15 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dto; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Server.Implementations.Devices; +using MediaBrowser.Server.Implementations.Playlists; namespace MediaBrowser.Server.Implementations.Persistence { @@ -95,11 +99,12 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _updateInheritedTagsCommand; public const int LatestSchemaVersion = 109; + private readonly IMemoryStreamProvider _memoryStreamProvider; /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> - public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector) + public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector, IMemoryStreamProvider memoryStreamProvider) : base(logManager, connector) { if (config == null) @@ -113,6 +118,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _config = config; _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); @@ -125,7 +131,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var cacheSize = _config.Configuration.SqliteCacheSize; if (cacheSize <= 0) { - cacheSize = Math.Min(Environment.ProcessorCount * 50000, 200000); + cacheSize = Math.Min(Environment.ProcessorCount * 50000, 100000); } var connection = await DbConnector.Connect(DbFilePath, false, false, 0 - cacheSize).ConfigureAwait(false); @@ -270,6 +276,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID"); _connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID"); _connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalSeriesId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ShortOverview", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Tagline", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Keywords", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProviderIds", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Images", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProductionLocations", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ThemeSongIds", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ThemeVideoIds", "Text"); _connection.AddColumn(Logger, "UserDataKeys", "Priority", "INT"); _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text"); @@ -412,7 +427,17 @@ namespace MediaBrowser.Server.Implementations.Persistence "SeriesId", "SeriesSortName", "PresentationUniqueKey", - "InheritedParentalRatingValue" + "InheritedParentalRatingValue", + "InheritedTags", + "ExternalSeriesId", + "ShortOverview", + "Tagline", + "Keywords", + "ProviderIds", + "Images", + "ProductionLocations", + "ThemeSongIds", + "ThemeVideoIds" }; private readonly string[] _mediaStreamSaveColumns = @@ -534,7 +559,16 @@ namespace MediaBrowser.Server.Implementations.Persistence "SeasonName", "SeasonId", "SeriesId", - "SeriesSortName" + "SeriesSortName", + "ExternalSeriesId", + "ShortOverview", + "Tagline", + "Keywords", + "ProviderIds", + "Images", + "ProductionLocations", + "ThemeSongIds", + "ThemeVideoIds" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -720,7 +754,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.Id; _saveItemCommand.GetParameter(index++).Value = item.GetType().FullName; - _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item); + + if (TypeRequiresDeserialization(item.GetType())) + { + _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } _saveItemCommand.GetParameter(index++).Value = item.Path; @@ -974,6 +1016,49 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = null; } + _saveItemCommand.GetParameter(index++).Value = item.ExternalSeriesId; + _saveItemCommand.GetParameter(index++).Value = item.ShortOverview; + _saveItemCommand.GetParameter(index++).Value = item.Tagline; + + if (item.Keywords.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Keywords.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = SerializeProviderIds(item); + _saveItemCommand.GetParameter(index++).Value = SerializeImages(item); + + if (item.ProductionLocations.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ProductionLocations.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (item.ThemeSongIds.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeSongIds.Select(i => i.ToString("N")).ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (item.ThemeVideoIds.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeVideoIds.Select(i => i.ToString("N")).ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + _saveItemCommand.Transaction = transaction; _saveItemCommand.ExecuteNonQuery(); @@ -1022,6 +1107,99 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + private string SerializeProviderIds(BaseItem item) + { + var ids = item.ProviderIds.ToList(); + + if (ids.Count == 0) + { + return null; + } + + return string.Join("|", ids.Select(i => i.Key + "=" + i.Value).ToArray()); + } + + private void DeserializeProviderIds(string value, BaseItem item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (item.ProviderIds.Count > 0) + { + return; + } + + var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + var idParts = part.Split('='); + + item.SetProviderId(idParts[0], idParts[1]); + } + } + + private string SerializeImages(BaseItem item) + { + var images = item.ImageInfos.ToList(); + + if (images.Count == 0) + { + return null; + } + + return string.Join("|", images.Select(ToValueString).ToArray()); + } + + private void DeserializeImages(string value, BaseItem item) + { + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (item.ImageInfos.Count > 0) + { + return; + } + + var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (var part in parts) + { + item.ImageInfos.Add(ItemImageInfoFromValueString(part)); + } + } + + public string ToValueString(ItemImageInfo image) + { + var delimeter = "*"; + + return (image.Path ?? string.Empty) + + delimeter + + image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + + delimeter + + image.Type + + delimeter + + image.IsPlaceholder; + } + + public ItemImageInfo ItemImageInfoFromValueString(string value) + { + var parts = value.Split(new[] { '*' }, StringSplitOptions.RemoveEmptyEntries); + + var image = new ItemImageInfo(); + + image.Path = parts[0]; + image.DateModified = new DateTime(long.Parse(parts[1], CultureInfo.InvariantCulture), DateTimeKind.Utc); + image.Type = (ImageType)Enum.Parse(typeof(ImageType), parts[2], true); + image.IsPlaceholder = string.Equals(parts[3], true.ToString(), StringComparison.OrdinalIgnoreCase); + + return image; + } + /// <summary> /// Internal retrieve from items or users table /// </summary> @@ -1056,6 +1234,91 @@ namespace MediaBrowser.Server.Implementations.Persistence private BaseItem GetItem(IDataReader reader) { + return GetItem(reader, new InternalItemsQuery()); + } + + private bool TypeRequiresDeserialization(Type type) + { + if (_config.Configuration.SkipDeserializationForBasicTypes) + { + if (type == typeof(MusicGenre)) + { + return false; + } + if (type == typeof(GameGenre)) + { + return false; + } + if (type == typeof(Genre)) + { + return false; + } + if (type == typeof(Studio)) + { + return false; + } + if (type == typeof(Year)) + { + return false; + } + if (type == typeof(Book)) + { + return false; + } + if (type == typeof(Person)) + { + return false; + } + if (type == typeof(RecordingGroup)) + { + return false; + } + if (type == typeof(Channel)) + { + return false; + } + if (type == typeof(ManualCollectionsFolder)) + { + return false; + } + if (type == typeof(CameraUploadsFolder)) + { + return false; + } + if (type == typeof(PlaylistsFolder)) + { + return false; + } + if (type == typeof(UserRootFolder)) + { + return false; + } + if (type == typeof(PhotoAlbum)) + { + return false; + } + if (type == typeof(Season)) + { + return false; + } + if (type == typeof(MusicArtist)) + { + return false; + } + } + if (_config.Configuration.SkipDeserializationForPrograms) + { + if (type == typeof(LiveTvProgram)) + { + return false; + } + } + + return true; + } + + private BaseItem GetItem(IDataReader reader, InternalItemsQuery query) + { var typeString = reader.GetString(0); var type = _typeMapper.GetType(typeString); @@ -1069,34 +1332,37 @@ namespace MediaBrowser.Server.Implementations.Persistence BaseItem item = null; - using (var stream = reader.GetMemoryStream(1)) + if (TypeRequiresDeserialization(type)) { - try - { - item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; - } - catch (SerializationException ex) - { - Logger.ErrorException("Error deserializing item", ex); - } - - if (item == null) + using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider)) { try { - item = Activator.CreateInstance(type) as BaseItem; + item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; } - catch + catch (SerializationException ex) { + Logger.ErrorException("Error deserializing item", ex); } } + } - if (item == null) + if (item == null) + { + try + { + item = Activator.CreateInstance(type) as BaseItem; + } + catch { - return null; } } + if (item == null) + { + return null; + } + if (!reader.IsDBNull(2)) { var hasStartDate = item as IHasStartDate; @@ -1170,194 +1436,275 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - if (!reader.IsDBNull(15)) - { - item.CommunityRating = reader.GetFloat(15); - } + var index = 15; - if (!reader.IsDBNull(16)) + if (!reader.IsDBNull(index)) { - item.CustomRating = reader.GetString(16); + item.CommunityRating = reader.GetFloat(index); } + index++; - if (!reader.IsDBNull(17)) + if (query.HasField(ItemFields.CustomRating)) { - item.IndexNumber = reader.GetInt32(17); + if (!reader.IsDBNull(index)) + { + item.CustomRating = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(18)) + if (!reader.IsDBNull(index)) { - item.IsLocked = reader.GetBoolean(18); + item.IndexNumber = reader.GetInt32(index); } + index++; - if (!reader.IsDBNull(19)) + if (query.HasField(ItemFields.Settings)) { - item.PreferredMetadataLanguage = reader.GetString(19); - } + if (!reader.IsDBNull(index)) + { + item.IsLocked = reader.GetBoolean(index); + } + index++; - if (!reader.IsDBNull(20)) - { - item.PreferredMetadataCountryCode = reader.GetString(20); + if (!reader.IsDBNull(index)) + { + item.PreferredMetadataLanguage = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.PreferredMetadataCountryCode = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(21)) + if (!reader.IsDBNull(index)) { - item.IsHD = reader.GetBoolean(21); + item.IsHD = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(22)) + if (!reader.IsDBNull(index)) { - item.ExternalEtag = reader.GetString(22); + item.ExternalEtag = reader.GetString(index); } + index++; - if (!reader.IsDBNull(23)) + if (!reader.IsDBNull(index)) { - item.DateLastRefreshed = reader.GetDateTime(23).ToUniversalTime(); + item.DateLastRefreshed = reader.GetDateTime(index).ToUniversalTime(); } + index++; - if (!reader.IsDBNull(24)) + if (!reader.IsDBNull(index)) { - item.Name = reader.GetString(24); + item.Name = reader.GetString(index); } + index++; - if (!reader.IsDBNull(25)) + if (!reader.IsDBNull(index)) { - item.Path = reader.GetString(25); + item.Path = reader.GetString(index); } + index++; - if (!reader.IsDBNull(26)) + if (!reader.IsDBNull(index)) { - item.PremiereDate = reader.GetDateTime(26).ToUniversalTime(); + item.PremiereDate = reader.GetDateTime(index).ToUniversalTime(); } + index++; - if (!reader.IsDBNull(27)) + if (query.HasField(ItemFields.Overview)) { - item.Overview = reader.GetString(27); + if (!reader.IsDBNull(index)) + { + item.Overview = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(28)) + if (!reader.IsDBNull(index)) { - item.ParentIndexNumber = reader.GetInt32(28); + item.ParentIndexNumber = reader.GetInt32(index); } + index++; - if (!reader.IsDBNull(29)) + if (!reader.IsDBNull(index)) { - item.ProductionYear = reader.GetInt32(29); + item.ProductionYear = reader.GetInt32(index); } + index++; - if (!reader.IsDBNull(30)) + if (!reader.IsDBNull(index)) { - item.OfficialRating = reader.GetString(30); + item.OfficialRating = reader.GetString(index); } + index++; - if (!reader.IsDBNull(31)) + if (query.HasField(ItemFields.OfficialRatingDescription)) { - item.OfficialRatingDescription = reader.GetString(31); + if (!reader.IsDBNull(index)) + { + item.OfficialRatingDescription = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(32)) + if (query.HasField(ItemFields.HomePageUrl)) { - item.HomePageUrl = reader.GetString(32); + if (!reader.IsDBNull(index)) + { + item.HomePageUrl = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(33)) + if (query.HasField(ItemFields.DisplayMediaType)) { - item.DisplayMediaType = reader.GetString(33); + if (!reader.IsDBNull(index)) + { + item.DisplayMediaType = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(34)) + if (query.HasField(ItemFields.SortName)) { - item.ForcedSortName = reader.GetString(34); + if (!reader.IsDBNull(index)) + { + item.ForcedSortName = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(35)) + if (!reader.IsDBNull(index)) { - item.RunTimeTicks = reader.GetInt64(35); + item.RunTimeTicks = reader.GetInt64(index); } + index++; - if (!reader.IsDBNull(36)) + if (query.HasField(ItemFields.VoteCount)) { - item.VoteCount = reader.GetInt32(36); + if (!reader.IsDBNull(index)) + { + item.VoteCount = reader.GetInt32(index); + } + index++; } - if (!reader.IsDBNull(37)) + if (query.HasField(ItemFields.DateCreated)) { - item.DateCreated = reader.GetDateTime(37).ToUniversalTime(); + if (!reader.IsDBNull(index)) + { + item.DateCreated = reader.GetDateTime(index).ToUniversalTime(); + } + index++; } - if (!reader.IsDBNull(38)) + if (!reader.IsDBNull(index)) { - item.DateModified = reader.GetDateTime(38).ToUniversalTime(); + item.DateModified = reader.GetDateTime(index).ToUniversalTime(); } + index++; - item.Id = reader.GetGuid(39); + item.Id = reader.GetGuid(index); + index++; - if (!reader.IsDBNull(40)) + if (query.HasField(ItemFields.Genres)) { - item.Genres = reader.GetString(40).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + if (!reader.IsDBNull(index)) + { + item.Genres = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; } - if (!reader.IsDBNull(41)) + if (!reader.IsDBNull(index)) { - item.ParentId = reader.GetGuid(41); + item.ParentId = reader.GetGuid(index); } + index++; - if (!reader.IsDBNull(42)) + if (!reader.IsDBNull(index)) { - item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(42), true); + item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(index), true); } + index++; - if (!reader.IsDBNull(43)) + if (query.HasField(ItemFields.ServiceName)) { - item.ServiceName = reader.GetString(43); + if (!reader.IsDBNull(index)) + { + item.ServiceName = reader.GetString(index); + } + index++; } - if (!reader.IsDBNull(44)) + if (!reader.IsDBNull(index)) { - item.IsInMixedFolder = reader.GetBoolean(44); + item.IsInMixedFolder = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(45)) + if (!reader.IsDBNull(index)) { - item.DateLastSaved = reader.GetDateTime(45).ToUniversalTime(); + item.DateLastSaved = reader.GetDateTime(index).ToUniversalTime(); } + index++; - if (!reader.IsDBNull(46)) + if (query.HasField(ItemFields.Settings)) { - item.LockedFields = reader.GetString(46).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList(); + if (!reader.IsDBNull(index)) + { + item.LockedFields = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList(); + } + index++; } - if (!reader.IsDBNull(47)) + if (query.HasField(ItemFields.Studios)) { - item.Studios = reader.GetString(47).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + if (!reader.IsDBNull(index)) + { + item.Studios = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; } - if (!reader.IsDBNull(48)) + if (query.HasField(ItemFields.Tags)) { - item.Tags = reader.GetString(48).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + if (!reader.IsDBNull(index)) + { + item.Tags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; } - if (!reader.IsDBNull(49)) + if (!reader.IsDBNull(index)) { - item.SourceType = (SourceType)Enum.Parse(typeof(SourceType), reader.GetString(49), true); + item.SourceType = (SourceType)Enum.Parse(typeof(SourceType), reader.GetString(index), true); } + index++; var trailer = item as Trailer; if (trailer != null) { - if (!reader.IsDBNull(50)) + if (!reader.IsDBNull(index)) { - trailer.TrailerTypes = reader.GetString(50).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList(); + trailer.TrailerTypes = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList(); } } + index++; - var index = 51; - - if (!reader.IsDBNull(index)) + if (query.HasField(ItemFields.OriginalTitle)) { - item.OriginalTitle = reader.GetString(index); + if (!reader.IsDBNull(index)) + { + item.OriginalTitle = reader.GetString(index); + } + index++; } - index++; var video = item as Video; if (video != null) @@ -1369,12 +1716,15 @@ namespace MediaBrowser.Server.Implementations.Persistence } index++; - var folder = item as Folder; - if (folder != null && !reader.IsDBNull(index)) + if (query.HasField(ItemFields.DateLastMediaAdded)) { - folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime(); + var folder = item as Folder; + if (folder != null && !reader.IsDBNull(index)) + { + folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime(); + } + index++; } - index++; if (!reader.IsDBNull(index)) { @@ -1388,11 +1738,14 @@ namespace MediaBrowser.Server.Implementations.Persistence } index++; - if (!reader.IsDBNull(index)) + if (query.HasField(ItemFields.CriticRatingSummary)) { - item.CriticRatingSummary = reader.GetString(index); + if (!reader.IsDBNull(index)) + { + item.CriticRatingSummary = reader.GetString(index); + } + index++; } - index++; if (!reader.IsDBNull(index)) { @@ -1459,6 +1812,105 @@ namespace MediaBrowser.Server.Implementations.Persistence } index++; + if (!reader.IsDBNull(index)) + { + item.InheritedTags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExternalSeriesId = reader.GetString(index); + } + index++; + + if (query.HasField(ItemFields.ShortOverview)) + { + if (!reader.IsDBNull(index)) + { + item.ShortOverview = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.Taglines)) + { + if (!reader.IsDBNull(index)) + { + item.Tagline = reader.GetString(index); + } + index++; + } + + if (query.HasField(ItemFields.Keywords)) + { + if (!reader.IsDBNull(index)) + { + item.Keywords = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (!reader.IsDBNull(index)) + { + DeserializeProviderIds(reader.GetString(index), item); + } + index++; + + if (query.DtoOptions.EnableImages) + { + if (!reader.IsDBNull(index)) + { + DeserializeImages(reader.GetString(index), item); + } + index++; + } + + if (query.HasField(ItemFields.ProductionLocations)) + { + if (!reader.IsDBNull(index)) + { + item.ProductionLocations = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.ThemeSongIds)) + { + if (!reader.IsDBNull(index)) + { + item.ThemeSongIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + } + index++; + } + + if (query.HasField(ItemFields.ThemeVideoIds)) + { + if (!reader.IsDBNull(index)) + { + item.ThemeVideoIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList(); + } + index++; + } + + if (string.IsNullOrWhiteSpace(item.Tagline)) + { + var movie = item as Movie; + if (movie != null && movie.Taglines.Count > 0) + { + movie.Tagline = movie.Taglines[0]; + } + } + + if (type == typeof(Person) && item.ProductionLocations.Count == 0) + { + var person = (Person)item; + if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth)) + { + item.ProductionLocations = new List<string> { person.PlaceOfBirth }; + } + } + return item; } @@ -1723,28 +2175,28 @@ namespace MediaBrowser.Server.Implementations.Persistence return true; } - if (query.SortBy != null && query.SortBy.Length > 0) + var sortingFields = query.SortBy.ToList(); + sortingFields.AddRange(query.OrderBy.Select(i => i.Item1)); + + if (sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)) { - if (query.SortBy.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (query.SortBy.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (query.SortBy.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (query.SortBy.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase)) - { - return true; - } - if (query.SortBy.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase)) - { - return true; - } + return true; + } + if (sortingFields.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + if (sortingFields.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase)) + { + return true; } if (query.IsFavoriteOrLiked.HasValue) @@ -1775,10 +2227,52 @@ namespace MediaBrowser.Server.Implementations.Persistence return false; } + private List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields)) + .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) + .ToList(); + + private IEnumerable<string> GetColumnNamesFromField(ItemFields field) + { + if (field == ItemFields.Settings) + { + return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" }; + } + if (field == ItemFields.ServiceName) + { + return new[] { "ExternalServiceId" }; + } + if (field == ItemFields.SortName) + { + return new[] { "ForcedSortName" }; + } + if (field == ItemFields.Taglines) + { + return new[] { "Tagline" }; + } + + return new[] { field.ToString() }; + } + private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns, IDbCommand cmd) { var list = startColumns.ToList(); + foreach (var field in allFields) + { + if (!query.HasField(field)) + { + foreach (var fieldToRemove in GetColumnNamesFromField(field).ToList()) + { + list.Remove(fieldToRemove); + } + } + } + + if (!query.DtoOptions.EnableImages) + { + list.Remove("Images"); + } + if (EnableJoinUserData(query)) { list.Add("UserDataDb.UserData.UserId"); @@ -1933,7 +2427,7 @@ namespace MediaBrowser.Server.Implementations.Persistence while (reader.Read()) { - var item = GetItem(reader); + var item = GetItem(reader, query); if (item != null) { list.Add(item); @@ -2120,7 +2614,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { while (reader.Read()) { - var item = GetItem(reader); + var item = GetItem(reader, query); if (item != null) { list.Add(item); @@ -2144,34 +2638,41 @@ namespace MediaBrowser.Server.Implementations.Persistence private string GetOrderByText(InternalItemsQuery query) { + var orderBy = query.OrderBy.ToList(); + var enableOrderInversion = true; + + if (orderBy.Count == 0) + { + orderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder))); + } + else + { + enableOrderInversion = false; + } + if (query.SimilarTo != null) { - if (query.SortBy == null || query.SortBy.Length == 0) + if (orderBy.Count == 0) { - if (query.User != null) - { - query.SortBy = new[] { "SimilarityScore", ItemSortBy.Random }; - } - else - { - query.SortBy = new[] { "SimilarityScore", ItemSortBy.Random }; - } + orderBy.Add(new Tuple<string, SortOrder>("SimilarityScore", SortOrder.Descending)); + orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending)); query.SortOrder = SortOrder.Descending; + enableOrderInversion = false; } } - if (query.SortBy == null || query.SortBy.Length == 0) + query.OrderBy = orderBy; + + if (orderBy.Count == 0) { return string.Empty; } - var isAscending = query.SortOrder != SortOrder.Descending; - - return " ORDER BY " + string.Join(",", query.SortBy.Select(i => + return " ORDER BY " + string.Join(",", orderBy.Select(i => { - var columnMap = MapOrderByField(i, query); - var columnAscending = isAscending; - if (columnMap.Item2) + var columnMap = MapOrderByField(i.Item1, query); + var columnAscending = i.Item2 == SortOrder.Ascending; + if (columnMap.Item2 && enableOrderInversion) { columnAscending = !columnAscending; } @@ -2523,38 +3024,116 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("IsOffline=@IsOffline"); cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline; } - if (query.IsMovie.HasValue) + + var exclusiveProgramAttribtues = !(query.IsMovie ?? true) || + !(query.IsSports ?? true) || + !(query.IsKids ?? true) || + !(query.IsNews ?? true) || + !(query.IsSeries ?? true); + + if (exclusiveProgramAttribtues) { - var alternateTypes = new List<string>(); - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + if (query.IsMovie.HasValue) + { + var alternateTypes = new List<string>(); + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + { + alternateTypes.Add(typeof(Movie).FullName); + } + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + { + alternateTypes.Add(typeof(Trailer).FullName); + } + + if (alternateTypes.Count == 0) + { + whereClauses.Add("IsMovie=@IsMovie"); + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + } + else + { + whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + } + } + if (query.IsSeries.HasValue) { - alternateTypes.Add(typeof(Movie).FullName); + whereClauses.Add("IsSeries=@IsSeries"); + cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = query.IsSeries; } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + if (query.IsNews.HasValue) { - alternateTypes.Add(typeof(Trailer).FullName); + whereClauses.Add("IsNews=@IsNews"); + cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = query.IsNews; } - - if (alternateTypes.Count == 0) + if (query.IsKids.HasValue) { - whereClauses.Add("IsMovie=@IsMovie"); + whereClauses.Add("IsKids=@IsKids"); + cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids; } - else + if (query.IsSports.HasValue) { - whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + whereClauses.Add("IsSports=@IsSports"); + cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; } - cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; } - if (query.IsKids.HasValue) + else { - whereClauses.Add("IsKids=@IsKids"); - cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids; + var programAttribtues = new List<string>(); + if (query.IsMovie ?? false) + { + var alternateTypes = new List<string>(); + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) + { + alternateTypes.Add(typeof(Movie).FullName); + } + if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + { + alternateTypes.Add(typeof(Trailer).FullName); + } + + if (alternateTypes.Count == 0) + { + programAttribtues.Add("IsMovie=@IsMovie"); + } + else + { + programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + } + + cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = true; + } + if (query.IsSports ?? false) + { + programAttribtues.Add("IsSports=@IsSports"); + cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = true; + } + if (query.IsNews ?? false) + { + programAttribtues.Add("IsNews=@IsNews"); + cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = true; + } + if (query.IsSeries ?? false) + { + programAttribtues.Add("IsSeries=@IsSeries"); + cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = true; + } + if (query.IsKids ?? false) + { + programAttribtues.Add("IsKids=@IsKids"); + cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = true; + } + if (programAttribtues.Count > 0) + { + whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray()) + ")"); + } } - if (query.IsSports.HasValue) + + if (query.SimilarTo != null) { - whereClauses.Add("IsSports=@IsSports"); - cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; + whereClauses.Add("SimilarityScore > 0"); } + if (query.IsFolder.HasValue) { whereClauses.Add("IsFolder=@IsFolder"); @@ -2626,6 +3205,18 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.Parameters.Add(cmd, "@MinIndexNumber", DbType.Int32).Value = query.MinIndexNumber.Value; } + if (query.MinDateCreated.HasValue) + { + whereClauses.Add("DateCreated>=@MinDateCreated"); + cmd.Parameters.Add(cmd, "@MinDateCreated", DbType.DateTime).Value = query.MinDateCreated.Value; + } + + if (query.MinDateLastSaved.HasValue) + { + whereClauses.Add("DateLastSaved>=@MinDateLastSaved"); + cmd.Parameters.Add(cmd, "@MinDateLastSaved", DbType.DateTime).Value = query.MinDateLastSaved.Value; + } + //if (query.MinPlayers.HasValue) //{ // whereClauses.Add("Players>=@MinPlayers"); @@ -2765,6 +3356,12 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.Parameters.Add(cmd, "@MinSortName", DbType.String).Value = query.MinSortName; } + if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) + { + whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); + cmd.Parameters.Add(cmd, "@ExternalSeriesId", DbType.String).Value = query.ExternalSeriesId; + } + if (!string.IsNullOrWhiteSpace(query.Name)) { whereClauses.Add("CleanName=@Name"); @@ -3235,6 +3832,28 @@ namespace MediaBrowser.Server.Implementations.Persistence clause += ")"; whereClauses.Add(clause); } + if (query.HasThemeSong.HasValue) + { + if (query.HasThemeSong.Value) + { + whereClauses.Add("ThemeSongIds not null"); + } + else + { + whereClauses.Add("ThemeSongIds is null"); + } + } + if (query.HasThemeVideo.HasValue) + { + if (query.HasThemeVideo.Value) + { + whereClauses.Add("ThemeVideoIds not null"); + } + else + { + whereClauses.Add("ThemeVideoIds is null"); + } + } //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; var enableItemsByName = query.IncludeItemsByName ?? false; diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs index 25ab60ca5..31fa78806 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs @@ -9,6 +9,7 @@ using System.Data; using System.IO; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Persistence { @@ -18,10 +19,12 @@ namespace MediaBrowser.Server.Implementations.Persistence public class SqliteUserRepository : BaseSqliteRepository, IUserRepository { private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamProvider _memoryStreamProvider; - public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector) : base(logManager, dbConnector) + public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector, IMemoryStreamProvider memoryStreamProvider) : base(logManager, dbConnector) { _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); } @@ -75,7 +78,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - var serialized = _jsonSerializer.SerializeToBytes(user); + var serialized = _jsonSerializer.SerializeToBytes(user, _memoryStreamProvider); cancellationToken.ThrowIfCancellationRequested(); @@ -150,7 +153,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var id = reader.GetGuid(0); - using (var stream = reader.GetMemoryStream(1)) + using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider)) { var user = _jsonSerializer.DeserializeFromStream<User>(stream); user.Id = id; diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index ff0e4a0e0..63dfe20b2 100644 --- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using CommonIO; +using MediaBrowser.Model.Querying; +using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Playlists { @@ -17,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.Playlists public override bool IsVisible(User user) { - return base.IsVisible(user) && GetChildren(user, false).Any(); + return base.IsVisible(user) && GetChildren(user, true).Any(); } protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) @@ -37,6 +39,12 @@ namespace MediaBrowser.Server.Implementations.Playlists { get { return Model.Entities.CollectionType.Playlists; } } + + protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query) + { + query.Recursive = false; + return base.GetItemsInternal(query); + } } public class PlaylistsDynamicFolder : IVirtualFolderCreator diff --git a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs index 33d106916..893592fa3 100644 --- a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs +++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.ServerManager { @@ -72,6 +73,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager private readonly List<IWebSocketListener> _webSocketListeners = new List<IWebSocketListener>(); private bool _disposed; + private readonly IMemoryStreamProvider _memoryStreamProvider; /// <summary> /// Initializes a new instance of the <see cref="ServerManager" /> class. @@ -81,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="logger">The logger.</param> /// <param name="configurationManager">The configuration manager.</param> /// <exception cref="System.ArgumentNullException">applicationHost</exception> - public ServerManager(IServerApplicationHost applicationHost, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager) + public ServerManager(IServerApplicationHost applicationHost, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager, IMemoryStreamProvider memoryStreamProvider) { if (applicationHost == null) { @@ -100,6 +102,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager _jsonSerializer = jsonSerializer; _applicationHost = applicationHost; ConfigurationManager = configurationManager; + _memoryStreamProvider = memoryStreamProvider; } /// <summary> @@ -150,7 +153,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager return; } - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger, _memoryStreamProvider) { OnReceive = ProcessWebSocketMessageReceived, Url = e.Url, diff --git a/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs b/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs index 2adf3e86a..60b04cf82 100644 --- a/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs +++ b/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs @@ -9,6 +9,7 @@ using System.Collections.Specialized; using System.IO; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.IO; using UniversalDetector; namespace MediaBrowser.Server.Implementations.ServerManager @@ -19,7 +20,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager public class WebSocketConnection : IWebSocketConnection { public event EventHandler<EventArgs> Closed; - + /// <summary> /// The _socket /// </summary> @@ -36,11 +37,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); /// <summary> - /// The _send semaphore - /// </summary> - private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1); - - /// <summary> /// The logger /// </summary> private readonly ILogger _logger; @@ -78,7 +74,8 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// </summary> /// <value>The query string.</value> public NameValueCollection QueryString { get; set; } - + private readonly IMemoryStreamProvider _memoryStreamProvider; + /// <summary> /// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// </summary> @@ -87,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> /// <exception cref="System.ArgumentNullException">socket</exception> - public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger) + public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamProvider memoryStreamProvider) { if (socket == null) { @@ -113,6 +110,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager _socket.OnReceive = OnReceiveInternal; RemoteEndPoint = remoteEndPoint; _logger = logger; + _memoryStreamProvider = memoryStreamProvider; socket.Closed += socket_Closed; } @@ -149,7 +147,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager { try { - using (var ms = new MemoryStream(bytes)) + using (var ms = _memoryStreamProvider.CreateNew(bytes)) { var detector = new CharsetDetector(); detector.Feed(ms); @@ -207,7 +205,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager _logger.ErrorException("Error processing web socket message", ex); } } - + /// <summary> /// Sends a message asynchronously. /// </summary> @@ -234,7 +232,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="buffer">The buffer.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public async Task SendAsync(byte[] buffer, CancellationToken cancellationToken) + public Task SendAsync(byte[] buffer, CancellationToken cancellationToken) { if (buffer == null) { @@ -243,33 +241,10 @@ namespace MediaBrowser.Server.Implementations.ServerManager cancellationToken.ThrowIfCancellationRequested(); - // Per msdn docs, attempting to send simultaneous messages will result in one failing. - // This should help us workaround that and ensure all messages get sent - await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await _socket.SendAsync(buffer, true, cancellationToken); - } - catch (OperationCanceledException) - { - _logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint); - - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint); - - throw; - } - finally - { - _sendSemaphore.Release(); - } + return _socket.SendAsync(buffer, true, cancellationToken); } - public async Task SendAsync(string text, CancellationToken cancellationToken) + public Task SendAsync(string text, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(text)) { @@ -278,30 +253,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager cancellationToken.ThrowIfCancellationRequested(); - // Per msdn docs, attempting to send simultaneous messages will result in one failing. - // This should help us workaround that and ensure all messages get sent - await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await _socket.SendAsync(text, true, cancellationToken); - } - catch (OperationCanceledException) - { - _logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint); - - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint); - - throw; - } - finally - { - _sendSemaphore.Release(); - } + return _socket.SendAsync(text, true, cancellationToken); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs index b841f0216..f54c452cc 100644 --- a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs +++ b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs @@ -75,7 +75,8 @@ namespace MediaBrowser.Server.Implementations.Session await _httpClient.Post(new HttpRequestOptions { Url = url, - CancellationToken = cancellationToken + CancellationToken = cancellationToken, + BufferContent = false }).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index afcdf9d90..e898a6abd 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -41,12 +41,7 @@ namespace MediaBrowser.Server.Implementations.Session /// <summary> /// The _user data repository /// </summary> - private readonly IUserDataManager _userDataRepository; - - /// <summary> - /// The _user repository - /// </summary> - private readonly IUserRepository _userRepository; + private readonly IUserDataManager _userDataManager; /// <summary> /// The _logger @@ -99,11 +94,10 @@ namespace MediaBrowser.Server.Implementations.Session private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); - public SessionManager(IUserDataManager userDataRepository, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager) + public SessionManager(IUserDataManager userDataManager, ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager) { - _userDataRepository = userDataRepository; + _userDataManager = userDataManager; _logger = logger; - _userRepository = userRepository; _libraryManager = libraryManager; _userManager = userManager; _musicManager = musicManager; @@ -248,13 +242,11 @@ namespace MediaBrowser.Server.Implementations.Session var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue; user.LastActivityDate = activityDate; - // Don't log in the db anymore frequently than 10 seconds - if ((activityDate - userLastActivityDate).TotalSeconds > 10) + if ((activityDate - userLastActivityDate).TotalSeconds > 60) { try { - // Save this directly. No need to fire off all the events for this. - await _userRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false); + await _userManager.UpdateUser(user).ConfigureAwait(false); } catch (Exception ex) { @@ -294,11 +286,9 @@ namespace MediaBrowser.Server.Implementations.Session var key = GetSessionKey(session.Client, session.DeviceId); SessionInfo removed; + _activeConnections.TryRemove(key, out removed); - if (_activeConnections.TryRemove(key, out removed)) - { - OnSessionEnded(removed); - } + OnSessionEnded(session); } } finally @@ -307,9 +297,9 @@ namespace MediaBrowser.Server.Implementations.Session } } - private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId) + private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId) { - return _mediaSourceManager.GetMediaSource(item, mediaSourceId, false); + return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None); } /// <summary> @@ -337,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Session var hasMediaSources = libraryItem as IHasMediaSources; if (hasMediaSources != null) { - mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId).ConfigureAwait(false); + mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); if (mediaSource != null) { @@ -638,17 +628,21 @@ namespace MediaBrowser.Server.Implementations.Session /// <returns>Task.</returns> private async Task OnPlaybackStart(Guid userId, IHasUserData item) { - var data = _userDataRepository.GetUserData(userId, item); + var data = _userDataManager.GetUserData(userId, item); data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; - if (!(item is Video)) + if (!(item is Video) && item.SupportsPlayedStatus) { data.Played = true; } + else + { + data.Played = false; + } - await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false); + await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -715,17 +709,17 @@ namespace MediaBrowser.Server.Implementations.Session private async Task OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) { - var data = _userDataRepository.GetUserData(user.Id, item); + var data = _userDataManager.GetUserData(user.Id, item); var positionTicks = info.PositionTicks; if (positionTicks.HasValue) { - _userDataRepository.UpdatePlayState(item, data, positionTicks.Value); + _userDataManager.UpdatePlayState(item, data, positionTicks.Value); UpdatePlaybackSettings(user, info, data); - await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false); + await _userDataManager.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false); } } @@ -792,7 +786,7 @@ namespace MediaBrowser.Server.Implementations.Session var hasMediaSources = libraryItem as IHasMediaSources; if (hasMediaSources != null) { - mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId).ConfigureAwait(false); + mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false); } info.Item = GetItemInfo(libraryItem, libraryItem, mediaSource); @@ -820,7 +814,7 @@ namespace MediaBrowser.Server.Implementations.Session { try { - await _mediaSourceManager.CloseLiveStream(info.LiveStreamId, CancellationToken.None).ConfigureAwait(false); + await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false); } catch (Exception ex) { @@ -851,22 +845,22 @@ namespace MediaBrowser.Server.Implementations.Session if (!playbackFailed) { - var data = _userDataRepository.GetUserData(userId, item); + var data = _userDataManager.GetUserData(userId, item); if (positionTicks.HasValue) { - playedToCompletion = _userDataRepository.UpdatePlayState(item, data, positionTicks.Value); + playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value); } - else + else { // If the client isn't able to report this, then we'll just have to make an assumption data.PlayCount++; - data.Played = true; + data.Played = item.SupportsPlayedStatus; data.PlaybackPositionTicks = 0; playedToCompletion = true; } - await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false); + await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false); } return playedToCompletion; diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index 1278a40a4..c523ec7bd 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -30,6 +30,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Sync { @@ -51,6 +52,7 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly Func<IMediaSourceManager> _mediaSourceManager; private readonly IJsonSerializer _json; private readonly ITaskManager _taskManager; + private readonly IMemoryStreamProvider _memoryStreamProvider; private ISyncProvider[] _providers = { }; @@ -60,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Sync public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemUpdated; public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemCreated; - public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func<IMediaSourceManager> mediaSourceManager, IJsonSerializer json, ITaskManager taskManager) + public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func<IMediaSourceManager> mediaSourceManager, IJsonSerializer json, ITaskManager taskManager, IMemoryStreamProvider memoryStreamProvider) { _libraryManager = libraryManager; _repo = repo; @@ -78,6 +80,7 @@ namespace MediaBrowser.Server.Implementations.Sync _mediaSourceManager = mediaSourceManager; _json = json; _taskManager = taskManager; + _memoryStreamProvider = memoryStreamProvider; } public void AddParts(IEnumerable<ISyncProvider> providers) @@ -95,7 +98,7 @@ namespace MediaBrowser.Server.Implementations.Sync public ISyncDataProvider GetDataProvider(IServerSyncProvider provider, SyncTarget target) { - return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths)); + return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths, _memoryStreamProvider)); } public async Task<SyncJobCreationResult> CreateJob(SyncJobRequest request) diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs index a2b5851ac..e0553b1b1 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Server.Implementations.Sync // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private const string StreamIdDelimeterString = "_"; - public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken) + public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken) { var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3); @@ -137,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Sync mediaSource.Protocol = dynamicInfo.Protocol; mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders; - return mediaSource; + return new Tuple<MediaSourceInfo, IDirectStreamProvider>(mediaSource, null); } private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource) @@ -150,7 +150,7 @@ namespace MediaBrowser.Server.Implementations.Sync } } - public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken) + public Task CloseMediaSource(string liveStreamId) { throw new NotImplementedException(); } diff --git a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs index 106dc9115..32a600371 100644 --- a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using Interfaces.IO; +using MediaBrowser.Common.IO; namespace MediaBrowser.Server.Implementations.Sync { @@ -28,8 +29,9 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly IFileSystem _fileSystem; private readonly IApplicationPaths _appPaths; private readonly IServerApplicationHost _appHost; + private readonly IMemoryStreamProvider _memoryStreamProvider; - public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths) + public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths, IMemoryStreamProvider memoryStreamProvider) { _logger = logger; _json = json; @@ -37,6 +39,7 @@ namespace MediaBrowser.Server.Implementations.Sync _target = target; _fileSystem = fileSystem; _appPaths = appPaths; + _memoryStreamProvider = memoryStreamProvider; _appHost = appHost; } @@ -90,7 +93,7 @@ namespace MediaBrowser.Server.Implementations.Sync private async Task SaveData(List<LocalItem> items, CancellationToken cancellationToken) { - using (var stream = new MemoryStream()) + using (var stream = _memoryStreamProvider.CreateNew()) { _json.SerializeToStream(items, stream); diff --git a/MediaBrowser.Server.Implementations/app.config b/MediaBrowser.Server.Implementations/app.config index 14f2f055f..77b8b9218 100644 --- a/MediaBrowser.Server.Implementations/app.config +++ b/MediaBrowser.Server.Implementations/app.config @@ -1,11 +1,11 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> - <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral" /> - <bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0" /> + <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral"/> + <bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0"/> </dependentAssembly> </assemblyBinding> </runtime> -</configuration>
\ No newline at end of file +<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/></startup></configuration> diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 746dc7f62..043257fc8 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -5,9 +5,8 @@ <package id="ini-parser" version="2.3.0" targetFramework="net45" />
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
<package id="MediaBrowser.Naming" version="1.0.0.55" targetFramework="net45" />
- <package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
<package id="morelinq" version="1.4.0" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
- <package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
- <package id="SocketHttpListener" version="1.0.0.39" targetFramework="net45" />
+ <package id="SimpleInjector" version="3.2.2" targetFramework="net45" />
+ <package id="SocketHttpListener" version="1.0.0.40" targetFramework="net45" />
</packages>
\ No newline at end of file |
