diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 2819a649aa..fb06e35ea3 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -56,6 +56,8 @@ + + @@ -82,6 +84,7 @@ + diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 04b6a656d5..19b339cd74 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -621,10 +621,27 @@ namespace MediaBrowser.Api.Playback /// The item. /// System.String. protected string GetUserAgentParam(BaseItem item) + { + var useragent = GetUserAgent(item); + + if (!string.IsNullOrEmpty(useragent)) + { + return "-user-agent \"" + useragent + "\""; + } + + return string.Empty; + } + + /// + /// Gets the user agent. + /// + /// The item. + /// System.String. + protected string GetUserAgent(BaseItem item) { if (item.Path.IndexOf("apple.com", StringComparison.OrdinalIgnoreCase) != -1) { - return "-user-agent \"QuickTime/7.6.2\""; + return "QuickTime/7.6.2"; } return string.Empty; diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 9bcce87c89..50a0c9accd 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -1,4 +1,7 @@ -using MediaBrowser.Api.Images; +using System.Net; +using System.Net.Cache; +using System.Net.Http; +using MediaBrowser.Api.Images; using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; @@ -188,6 +191,11 @@ namespace MediaBrowser.Api.Playback.Progressive var responseHeaders = new Dictionary(); + if (request.Static && state.Item.LocationType == LocationType.Remote) + { + return GetStaticRemoteStreamResult(state.Item, responseHeaders, isHeadRequest).Result; + } + var outputPath = GetOutputFilePath(state); var outputPathExists = File.Exists(outputPath); @@ -209,6 +217,60 @@ namespace MediaBrowser.Api.Playback.Progressive return GetStreamResult(state, responseHeaders, isHeadRequest).Result; } + /// + /// Gets the static remote stream result. + /// + /// The item. + /// The response headers. + /// if set to true [is head request]. + /// Task{System.Object}. + private async Task GetStaticRemoteStreamResult(BaseItem item, Dictionary responseHeaders, bool isHeadRequest) + { + responseHeaders["Accept-Ranges"] = "none"; + + var httpClient = new HttpClient(new WebRequestHandler + { + CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache), + AutomaticDecompression = DecompressionMethods.None + }); + + using (var message = new HttpRequestMessage(HttpMethod.Get, item.Path)) + { + var useragent = GetUserAgent(item); + + if (!string.IsNullOrEmpty(useragent)) + { + message.Headers.Add("User-Agent", useragent); + } + + var response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var contentType = response.Content.Headers.ContentType.MediaType; + + // Headers only + if (isHeadRequest) + { + response.Dispose(); + + return ResultFactory.GetResult(null, contentType, responseHeaders); + } + + var result = new StaticRemoteStreamWriter(response, httpClient); + + result.Options["Content-Type"] = contentType; + + // Add the response headers to the result object + foreach (var header in responseHeaders) + { + result.Options[header.Key] = header.Value; + } + + return result; + } + } + /// /// Gets the album art response. /// diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs new file mode 100644 index 0000000000..89e13acb38 --- /dev/null +++ b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs @@ -0,0 +1,75 @@ +using ServiceStack.Service; +using ServiceStack.ServiceHost; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Playback +{ + /// + /// Class StaticRemoteStreamWriter + /// + public class StaticRemoteStreamWriter : IStreamWriter, IHasOptions + { + /// + /// The _input stream + /// + private readonly HttpResponseMessage _msg; + + private readonly HttpClient _client; + + /// + /// The _options + /// + private readonly IDictionary _options = new Dictionary(); + + /// + /// Initializes a new instance of the class. + /// + public StaticRemoteStreamWriter(HttpResponseMessage msg, HttpClient client) + { + _msg = msg; + _client = client; + } + + /// + /// Gets the options. + /// + /// The options. + public IDictionary Options + { + get { return _options; } + } + + /// + /// Writes to. + /// + /// The response stream. + public void WriteTo(Stream responseStream) + { + var task = WriteToAsync(responseStream); + + Task.WaitAll(task); + } + + /// + /// Writes to async. + /// + /// The response stream. + /// Task. + public async Task WriteToAsync(Stream responseStream) + { + using (_client) + { + using (_msg) + { + using (var input = await _msg.Content.ReadAsStreamAsync().ConfigureAwait(false)) + { + await input.CopyToAsync(responseStream).ConfigureAwait(false); + } + } + } + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 22098a368e..26b0aa1921 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -135,7 +135,7 @@ namespace MediaBrowser.Api.UserLibrary { if (!string.IsNullOrEmpty(request.NameStartsWithOrGreater)) { - items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.Name, StringComparison.OrdinalIgnoreCase) < 1); + items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.Name, StringComparison.CurrentCultureIgnoreCase) < 1); } var filters = request.GetFilters().ToList(); diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index c57778fd65..fbf41eb38a 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -456,7 +456,7 @@ namespace MediaBrowser.Api.UserLibrary if (!string.IsNullOrEmpty(request.NameStartsWithOrGreater)) { - items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.SortName, StringComparison.OrdinalIgnoreCase) < 1); + items = items.Where(i => string.Compare(request.NameStartsWithOrGreater, i.SortName, StringComparison.CurrentCultureIgnoreCase) < 1); } // Filter by Series Status diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs index 4c51d1299a..82a91fa46a 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -97,7 +97,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager }; client = new HttpClient(handler); - client.Timeout = TimeSpan.FromSeconds(30); + client.Timeout = TimeSpan.FromSeconds(15); _httpClients.TryAdd(host, client); } diff --git a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs index f63a47627e..c03e2a7f5e 100644 --- a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs +++ b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs @@ -63,8 +63,15 @@ namespace MediaBrowser.Controller.Providers.TV } case "Airs_Time": - item.AirTime = reader.ReadElementContentAsString(); - break; + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.AirTime = val; + } + break; + } case "SeriesName": item.Name = reader.ReadElementContentAsString(); diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 8227170d41..f28721f5ff 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Session /// The device id. /// Name of the device. /// - void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName); + Task OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName); /// /// Used to report playback progress for an item diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 2f9c7e3895..d3dbbc62b8 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -200,7 +200,7 @@ namespace MediaBrowser.Server.Implementations.Session /// Name of the device. /// /// - public void OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName) + public async Task OnPlaybackStart(User user, BaseItem item, string clientType, string deviceId, string deviceName) { if (user == null) { @@ -213,6 +213,15 @@ namespace MediaBrowser.Server.Implementations.Session UpdateNowPlayingItemId(user, clientType, deviceId, deviceName, item, false); + var key = item.GetUserDataKey(); + + var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); + + data.PlayCount++; + data.LastPlayedDate = DateTime.UtcNow; + + await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); + // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs @@ -254,7 +263,7 @@ namespace MediaBrowser.Server.Implementations.Session { var data = await _userDataRepository.GetUserData(user.Id, key).ConfigureAwait(false); - UpdatePlayState(item, data, positionTicks.Value, false); + UpdatePlayState(item, data, positionTicks.Value); await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false); } @@ -297,7 +306,7 @@ namespace MediaBrowser.Server.Implementations.Session if (positionTicks.HasValue) { - UpdatePlayState(item, data, positionTicks.Value, true); + UpdatePlayState(item, data, positionTicks.Value); } else { @@ -322,11 +331,12 @@ namespace MediaBrowser.Server.Implementations.Session /// The item /// User data for the item /// The current playback position - /// Whether or not to increment playcount - private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks, bool incrementPlayCount) + private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks) { + var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0; + // If a position has been reported, and if we know the duration - if (positionTicks > 0 && item.RunTimeTicks.HasValue && item.RunTimeTicks > 0) + if (positionTicks > 0 && hasRuntime) { var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100; @@ -334,7 +344,6 @@ namespace MediaBrowser.Server.Implementations.Session if (pctIn < _configurationManager.Configuration.MinResumePct) { positionTicks = 0; - incrementPlayCount = false; } // If we're at the end, assume completed @@ -356,19 +365,19 @@ namespace MediaBrowser.Server.Implementations.Session } } } + else if (!hasRuntime) + { + // If we don't know the runtime we'll just have to assume it was fully played + data.Played = true; + positionTicks = 0; + } if (item is Audio) { - data.PlaybackPositionTicks = 0; + positionTicks = 0; } data.PlaybackPositionTicks = positionTicks; - - if (incrementPlayCount) - { - data.PlayCount++; - data.LastPlayedDate = DateTime.UtcNow; - } } } }