diff --git a/MediaBrowser.ServerApplication/Native/LnkShortcutHandler.cs b/Emby.Common.Implementations/IO/LnkShortcutHandler.cs similarity index 99% rename from MediaBrowser.ServerApplication/Native/LnkShortcutHandler.cs rename to Emby.Common.Implementations/IO/LnkShortcutHandler.cs index b4a87b9b4..5d5f46057 100644 --- a/MediaBrowser.ServerApplication/Native/LnkShortcutHandler.cs +++ b/Emby.Common.Implementations/IO/LnkShortcutHandler.cs @@ -6,7 +6,7 @@ using System.Security; using System.Text; using MediaBrowser.Model.IO; -namespace MediaBrowser.ServerApplication.Native +namespace Emby.Common.Implementations.IO { public class LnkShortcutHandler :IShortcutHandler { @@ -35,7 +35,6 @@ namespace MediaBrowser.ServerApplication.Native /// /// Class NativeMethods /// - [SuppressUnmanagedCodeSecurity] public static class NativeMethods { /// diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs index 3fe20f659..0c1c02cd5 100644 --- a/Emby.Common.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; namespace Emby.Common.Implementations.IO { @@ -18,17 +19,21 @@ namespace Emby.Common.Implementations.IO private readonly bool _supportsAsyncFileStreams; private char[] _invalidFileNameChars; private readonly List _shortcutHandlers = new List(); - private bool EnableFileSystemRequestConcat = true; + private bool EnableFileSystemRequestConcat; private string _tempPath; - public ManagedFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, bool enableFileSystemRequestConcat, string tempPath) + public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath) { Logger = logger; - _supportsAsyncFileStreams = supportsAsyncFileStreams; + _supportsAsyncFileStreams = true; _tempPath = tempPath; - EnableFileSystemRequestConcat = enableFileSystemRequestConcat; - SetInvalidFileNameChars(enableManagedInvalidFileNameChars); + + // On Linux, this needs to be true or symbolic links are ignored + EnableFileSystemRequestConcat = environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows && + environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX; + + SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows); } public void AddShortcutHandler(IShortcutHandler handler) diff --git a/Emby.Common.Implementations/Net/NetAcceptSocket.cs b/Emby.Common.Implementations/Net/NetAcceptSocket.cs index 731ad1b74..3721709e6 100644 --- a/Emby.Common.Implementations/Net/NetAcceptSocket.cs +++ b/Emby.Common.Implementations/Net/NetAcceptSocket.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; using Emby.Common.Implementations.Networking; using MediaBrowser.Model.Net; using MediaBrowser.Model.Logging; @@ -59,7 +60,7 @@ namespace Emby.Common.Implementations.Net #if NET46 Socket.Close(); #else - Socket.Dispose(); + Socket.Dispose(); #endif } @@ -96,6 +97,46 @@ namespace Emby.Common.Implementations.Net _acceptor.StartAccept(); } +#if NET46 + public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken) + { + var options = TransmitFileOptions.UseKernelApc; + + var completionSource = new TaskCompletionSource(); + + var result = Socket.BeginSendFile(path, preBuffer, postBuffer, options, new AsyncCallback(FileSendCallback), new Tuple>(Socket, path, completionSource)); + + return completionSource.Task; + } + + private void FileSendCallback(IAsyncResult ar) + { + // Retrieve the socket from the state object. + Tuple> data = (Tuple>)ar.AsyncState; + + var client = data.Item1; + var path = data.Item2; + var taskCompletion = data.Item3; + + // Complete sending the data to the remote device. + try { + client.EndSendFile(ar); + taskCompletion.TrySetResult(true); +} + catch(SocketException ex){ + _logger.Info("Socket.SendFile failed for {0}. error code {1}", path, ex.SocketErrorCode); + taskCompletion.TrySetException(ex); +}catch(Exception ex){ + taskCompletion.TrySetException(ex); +} + } +#else + public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +#endif + public void Dispose() { Socket.Dispose(); diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs index 523f4da85..1fd367afb 100644 --- a/Emby.Common.Implementations/Net/SocketFactory.cs +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -52,6 +52,18 @@ namespace Emby.Common.Implementations.Net { throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); } + catch (ArgumentException ex) + { + if (dualMode) + { + // Mono for BSD incorrectly throws ArgumentException instead of SocketException + throw new SocketCreateException("AddressFamilyNotSupported", ex); + } + else + { + throw; + } + } } public ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort) @@ -59,6 +71,7 @@ namespace Emby.Common.Implementations.Net if (remotePort < 0) throw new ArgumentException("remotePort cannot be less than zero.", "remotePort"); var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + try { retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); @@ -96,10 +109,31 @@ namespace Emby.Common.Implementations.Net } } + public ISocket CreateUdpBroadcastSocket(int localPort) + { + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + try + { + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); + + return new UdpSocket(retVal, localPort, IPAddress.Any); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + /// - /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port. - /// - /// An implementation of the interface used by RSSDP components to perform acceptSocket operations. + /// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port. + /// + /// An implementation of the interface used by RSSDP components to perform acceptSocket operations. public ISocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort) { if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs index 8e2a1da6f..f9181eb6a 100644 --- a/Emby.Common.Implementations/Net/UdpSocket.cs +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -16,12 +16,23 @@ namespace Emby.Common.Implementations.Net internal sealed class UdpSocket : DisposableManagedObjectBase, ISocket { - - #region Fields - private Socket _Socket; private int _LocalPort; - #endregion + + private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs() + { + SocketFlags = SocketFlags.None + }; + + private readonly SocketAsyncEventArgs _sendSocketAsyncEventArgs = new SocketAsyncEventArgs() + { + SocketFlags = SocketFlags.None + }; + + private TaskCompletionSource _currentReceiveTaskCompletionSource; + private TaskCompletionSource _currentSendTaskCompletionSource; + + private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); public UdpSocket(Socket socket, int localPort, IPAddress ip) { @@ -32,6 +43,61 @@ namespace Emby.Common.Implementations.Net LocalIPAddress = NetworkManager.ToIpAddressInfo(ip); _Socket.Bind(new IPEndPoint(ip, _LocalPort)); + + InitReceiveSocketAsyncEventArgs(); + } + + private void InitReceiveSocketAsyncEventArgs() + { + var receiveBuffer = new byte[8192]; + _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length); + _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed; + + var sendBuffer = new byte[8192]; + _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length); + _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed; + } + + private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) + { + var tcs = _currentReceiveTaskCompletionSource; + if (tcs != null) + { + _currentReceiveTaskCompletionSource = null; + + if (e.SocketError == SocketError.Success) + { + tcs.TrySetResult(new SocketReceiveResult + { + Buffer = e.Buffer, + ReceivedBytes = e.BytesTransferred, + RemoteEndPoint = ToIpEndPointInfo(e.RemoteEndPoint as IPEndPoint), + LocalIPAddress = LocalIPAddress + }); + } + else + { + tcs.TrySetException(new Exception("SocketError: " + e.SocketError)); + } + } + } + + private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) + { + var tcs = _currentSendTaskCompletionSource; + if (tcs != null) + { + _currentSendTaskCompletionSource = null; + + if (e.SocketError == SocketError.Success) + { + tcs.TrySetResult(e.BytesTransferred); + } + else + { + tcs.TrySetException(new Exception("SocketError: " + e.SocketError)); + } + } } public UdpSocket(Socket socket, IpEndPointInfo endPoint) @@ -40,6 +106,8 @@ namespace Emby.Common.Implementations.Net _Socket = socket; _Socket.Connect(NetworkManager.ToIPEndPoint(endPoint)); + + InitReceiveSocketAsyncEventArgs(); } public IpAddressInfo LocalIPAddress @@ -48,32 +116,33 @@ namespace Emby.Common.Implementations.Net private set; } - #region ISocket Members - public Task ReceiveAsync(CancellationToken cancellationToken) { ThrowIfDisposed(); - var tcs = new TaskCompletionSource(); - EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); + var state = new AsyncReceiveState(_Socket, receivedFromEndPoint); state.TaskCompletionSource = tcs; -#if NETSTANDARD1_6 - _Socket.ReceiveFromAsync(new ArraySegment(state.Buffer), SocketFlags.None, state.RemoteEndPoint) - .ContinueWith((task, asyncState) => + cancellationToken.Register(() => tcs.TrySetCanceled()); + + _receiveSocketAsyncEventArgs.RemoteEndPoint = receivedFromEndPoint; + _currentReceiveTaskCompletionSource = tcs; + + try + { + var willRaiseEvent = _Socket.ReceiveFromAsync(_receiveSocketAsyncEventArgs); + + if (!willRaiseEvent) { - if (task.Status != TaskStatus.Faulted) - { - var receiveState = asyncState as AsyncReceiveState; - receiveState.RemoteEndPoint = task.Result.RemoteEndPoint; - ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress); - } - }, state); -#else - _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state); -#endif + _receiveSocketAsyncEventArgs_Completed(this, _receiveSocketAsyncEventArgs); + } + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } return tcs.Task; } @@ -129,15 +198,48 @@ namespace Emby.Common.Implementations.Net taskSource.TrySetException(ex); } - //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port)); - return taskSource.Task; #endif + //ThrowIfDisposed(); + + //if (buffer == null) throw new ArgumentNullException("messageData"); + //if (endPoint == null) throw new ArgumentNullException("endPoint"); + + //cancellationToken.ThrowIfCancellationRequested(); + + //var tcs = new TaskCompletionSource(); + + //cancellationToken.Register(() => tcs.TrySetCanceled()); + + //_sendSocketAsyncEventArgs.SetBuffer(buffer, 0, size); + //_sendSocketAsyncEventArgs.RemoteEndPoint = NetworkManager.ToIPEndPoint(endPoint); + //_currentSendTaskCompletionSource = tcs; + + //var willRaiseEvent = _Socket.SendAsync(_sendSocketAsyncEventArgs); + + //if (!willRaiseEvent) + //{ + // _sendSocketAsyncEventArgs_Completed(this, _sendSocketAsyncEventArgs); + //} + + //return tcs.Task; } - #endregion + public async Task SendWithLockAsync(byte[] buffer, int size, IpEndPointInfo endPoint, CancellationToken cancellationToken) + { + ThrowIfDisposed(); - #region Overrides + //await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await SendAsync(buffer, size, endPoint, cancellationToken).ConfigureAwait(false); + } + finally + { + //_sendLock.Release(); + } + } protected override void Dispose(bool disposing) { @@ -146,44 +248,19 @@ namespace Emby.Common.Implementations.Net var socket = _Socket; if (socket != null) socket.Dispose(); - } - } - #endregion + _sendLock.Dispose(); - #region Private Methods - - private static void ProcessResponse(AsyncReceiveState state, Func receiveData, IpAddressInfo localIpAddress) - { - try - { - var bytesRead = receiveData(); - - var ipEndPoint = state.RemoteEndPoint as IPEndPoint; - state.TaskCompletionSource.SetResult( - new SocketReceiveResult - { - Buffer = state.Buffer, - ReceivedBytes = bytesRead, - RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), - LocalIPAddress = localIpAddress - } - ); - } - catch (ObjectDisposedException) - { - state.TaskCompletionSource.SetCanceled(); - } - catch (SocketException se) - { - if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown) - state.TaskCompletionSource.SetException(se); - else - state.TaskCompletionSource.SetCanceled(); - } - catch (Exception ex) - { - state.TaskCompletionSource.SetException(ex); + var tcs = _currentReceiveTaskCompletionSource; + if (tcs != null) + { + tcs.TrySetCanceled(); + } + var sendTcs = _currentSendTaskCompletionSource; + if (sendTcs != null) + { + sendTcs.TrySetCanceled(); + } } } @@ -227,10 +304,6 @@ namespace Emby.Common.Implementations.Net #endif } - #endregion - - #region Private Classes - private class AsyncReceiveState { public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint) @@ -247,8 +320,5 @@ namespace Emby.Common.Implementations.Net public TaskCompletionSource TaskCompletionSource { get; set; } } - - #endregion - } } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index d048dcde1..c7eae91dd 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -208,7 +208,7 @@ namespace Emby.Dlna.Didl var targetHeight = streamInfo.TargetHeight; var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container, - streamInfo.VideoCodec, + streamInfo.TargetVideoCodec, streamInfo.TargetAudioCodec, targetWidth, targetHeight, @@ -352,7 +352,7 @@ namespace Emby.Dlna.Didl var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, streamInfo.TargetAudioCodec, - streamInfo.VideoCodec, + streamInfo.TargetVideoCodec, streamInfo.TargetAudioBitrate, targetWidth, targetHeight, diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index 3c07e95db..b73332c4b 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -542,7 +542,7 @@ namespace Emby.Dlna.PlayTo { var list = new ContentFeatureBuilder(profile) .BuildVideoHeader(streamInfo.Container, - streamInfo.VideoCodec, + streamInfo.TargetVideoCodec, streamInfo.TargetAudioCodec, streamInfo.TargetWidth, streamInfo.TargetHeight, diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index a15f75c9a..5b04ceea2 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -238,7 +238,7 @@ namespace Emby.Drawing var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]); var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); - var imageProcessingLockTaken = false; + //var imageProcessingLockTaken = false; try { @@ -253,9 +253,9 @@ namespace Emby.Drawing var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(cacheFilePath)); _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); - await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); + //await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); - imageProcessingLockTaken = true; + //imageProcessingLockTaken = true; _imageEncoder.EncodeImage(originalImagePath, tmpPath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat); CopyFile(tmpPath, cacheFilePath); @@ -273,13 +273,13 @@ namespace Emby.Drawing // Just spit out the original file if all the options are default return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - finally - { - if (imageProcessingLockTaken) - { - _imageProcessingSemaphore.Release(); - } - } + //finally + //{ + // if (imageProcessingLockTaken) + // { + // _imageProcessingSemaphore.Release(); + // } + //} } private void CopyFile(string src, string destination) diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 4425d1a0b..50c572b8c 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -61,7 +61,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Common.Implementations; using Emby.Common.Implementations.Archiving; -using Emby.Common.Implementations.Networking; +using Emby.Common.Implementations.IO; using Emby.Common.Implementations.Reflection; using Emby.Common.Implementations.Serialization; using Emby.Common.Implementations.TextEncoding; @@ -93,7 +93,7 @@ using Emby.Server.Implementations.Social; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Dto; -using Emby.Server.Implementations.EntryPoints; +using Emby.Server.Implementations.IO; using Emby.Server.Implementations.FileOrganization; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; @@ -107,7 +107,6 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations; using Emby.Server.Implementations.ServerManager; using Emby.Server.Implementations.Session; -using Emby.Server.Implementations.Social; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using MediaBrowser.Model.Activity; @@ -294,6 +293,13 @@ namespace Emby.Server.Core ImageEncoder = imageEncoder; SetBaseExceptionMessage(); + + if (environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + fileSystem.AddShortcutHandler(new LnkShortcutHandler()); + } + + fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); } private Version _version; @@ -606,7 +612,7 @@ namespace Emby.Server.Core CertificatePath = GetCertificatePath(true); Certificate = GetCertificate(CertificatePath); - HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets); + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, FileSystemManager, SupportsDualModeSockets); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); RegisterSingleInstance(HttpServer, false); progress.Report(10); @@ -796,17 +802,25 @@ namespace Emby.Server.Core info.FFMpegFilename = "ffmpeg"; info.FFProbeFilename = "ffprobe"; info.ArchiveType = "7z"; - info.Version = "20160215"; + info.Version = "20170308"; info.DownloadUrls = GetLinuxDownloadUrls(); } else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) { info.FFMpegFilename = "ffmpeg.exe"; info.FFProbeFilename = "ffprobe.exe"; - info.Version = "20160410"; + info.Version = "20170308"; info.ArchiveType = "7z"; info.DownloadUrls = GetWindowsDownloadUrls(); } + else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX) + { + info.FFMpegFilename = "ffmpeg"; + info.FFProbeFilename = "ffprobe"; + info.ArchiveType = "7z"; + info.Version = "20170308"; + info.DownloadUrls = GetMacDownloadUrls(); + } else { // No version available - user requirement @@ -816,6 +830,20 @@ namespace Emby.Server.Core return info; } + private string[] GetMacDownloadUrls() + { + switch (EnvironmentInfo.SystemArchitecture) + { + case Architecture.X64: + return new[] + { + "https://embydata.com/downloads/ffmpeg/osx/ffmpeg-x64-20170308.7z" + }; + } + + return new string[] { }; + } + private string[] GetWindowsDownloadUrls() { switch (EnvironmentInfo.SystemArchitecture) @@ -823,12 +851,12 @@ namespace Emby.Server.Core case Architecture.X64: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win64.7z" + "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win64.7z" }; case Architecture.X86: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win32.7z" + "https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win32.7z" }; } @@ -842,12 +870,12 @@ namespace Emby.Server.Core case Architecture.X64: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-64bit-static.7z" + "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-64bit-static.7z" }; case Architecture.X86: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-32bit-static.7z" + "https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-32bit-static.7z" }; } @@ -1714,14 +1742,8 @@ namespace Emby.Server.Core ((IProcess)sender).Dispose(); } - public void EnableLoopback(string appName) + public virtual void EnableLoopback(string appName) { - EnableLoopbackInternal(appName); - } - - protected virtual void EnableLoopbackInternal(string appName) - { - } private void RegisterModules() diff --git a/Emby.Server.Core/HttpServerFactory.cs b/Emby.Server.Core/HttpServerFactory.cs index deed3c6f3..dfd435c33 100644 --- a/Emby.Server.Core/HttpServerFactory.cs +++ b/Emby.Server.Core/HttpServerFactory.cs @@ -45,6 +45,7 @@ namespace Emby.Server.Core IXmlSerializer xml, IEnvironmentInfo environment, ICertificate certificate, + IFileSystem fileSystem, bool enableDualModeSockets) { var logger = logManager.GetLogger("HttpServer"); @@ -65,7 +66,8 @@ namespace Emby.Server.Core certificate, new StreamFactory(), GetParseFn, - enableDualModeSockets); + enableDualModeSockets, + fileSystem); } private static Func GetParseFn(Type propertyType) diff --git a/Emby.Server.Core/IO/LibraryMonitor.cs b/Emby.Server.Core/IO/LibraryMonitor.cs index 4df9b930e..e1e3186c3 100644 --- a/Emby.Server.Core/IO/LibraryMonitor.cs +++ b/Emby.Server.Core/IO/LibraryMonitor.cs @@ -421,17 +421,6 @@ namespace Emby.Server.Core.IO var path = e.FullPath; - // For deletes, use the parent path - if (e.ChangeType == WatcherChangeTypes.Deleted) - { - var parentPath = Path.GetDirectoryName(path); - - if (!string.IsNullOrWhiteSpace(parentPath)) - { - path = parentPath; - } - } - ReportFileSystemChanged(path); } catch (Exception ex) diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 2819a249f..0096f2284 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; @@ -22,13 +23,15 @@ namespace Emby.Server.Implementations.Data private readonly IItemRepository _itemRepo; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; _fileSystem = fileSystem; + _appPaths = appPaths; } public string Name @@ -150,13 +153,27 @@ namespace Emby.Server.Implementations.Data try { - if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path)) + var isPathInLibrary = false; + + if (allLibraryPaths.Any(i => path.StartsWith(i, StringComparison.Ordinal)) || + allLibraryPaths.Contains(path, StringComparer.Ordinal) || + path.StartsWith(_appPaths.ProgramDataPath, StringComparison.Ordinal)) { - continue; + isPathInLibrary = true; + + if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path)) + { + continue; + } } var libraryItem = _libraryManager.GetItemById(item.Item1); + if (libraryItem == null) + { + continue; + } + if (libraryItem.IsTopParent) { continue; @@ -180,7 +197,14 @@ namespace Emby.Server.Implementations.Data continue; } - _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty); + if (isPathInLibrary) + { + _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty); + } + else + { + _logger.Info("Deleting item from database {0} because path is no longer in the server library. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty); + } await libraryItem.OnFileDeleted().ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e65ebeb04..c5ba6c892 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3204,6 +3204,40 @@ namespace Emby.Server.Implementations.Data } } + private bool IsAlphaNumeric(string str) + { + if (string.IsNullOrWhiteSpace(str)) + return false; + + for (int i = 0; i < str.Length; i++) + { + if (!(char.IsLetter(str[i])) && (!(char.IsNumber(str[i])))) + return false; + } + + return true; + } + + private bool IsValidType(string value) + { + return IsAlphaNumeric(value); + } + + private bool IsValidMediaType(string value) + { + return IsAlphaNumeric(value); + } + + private bool IsValidId(string value) + { + return IsAlphaNumeric(value); + } + + private bool IsValidPersonType(string value) + { + return IsAlphaNumeric(value); + } + private List GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "") { if (query.IsResumable ?? false) @@ -3423,9 +3457,9 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@ChannelId", query.ChannelIds[0]); } } - if (query.ChannelIds.Length > 1) + else if (query.ChannelIds.Length > 1) { - var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i + "'").ToArray()); + var inClause = string.Join(",", query.ChannelIds.Where(IsValidId).Select(i => "'" + i + "'").ToArray()); whereClauses.Add(string.Format("ChannelId in ({0})", inClause)); } @@ -4157,17 +4191,18 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))"); } } - if (query.MediaTypes.Length == 1) + var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray(); + if (queryMediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); if (statement != null) { - statement.TryBind("@MediaTypes", query.MediaTypes[0]); + statement.TryBind("@MediaTypes", queryMediaTypes[0]); } } - if (query.MediaTypes.Length > 1) + else if (queryMediaTypes.Length > 1) { - var val = string.Join(",", query.MediaTypes.Select(i => "'" + i + "'").ToArray()); + var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add("MediaType in (" + val + ")"); } @@ -4273,7 +4308,9 @@ namespace Emby.Server.Implementations.Data //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; var enableItemsByName = query.IncludeItemsByName ?? false; - if (query.TopParentIds.Length == 1) + var queryTopParentIds = query.TopParentIds.Where(IsValidId).ToArray(); + + if (queryTopParentIds.Length == 1) { if (enableItemsByName) { @@ -4289,12 +4326,12 @@ namespace Emby.Server.Implementations.Data } if (statement != null) { - statement.TryBind("@TopParentId", query.TopParentIds[0]); + statement.TryBind("@TopParentId", queryTopParentIds[0]); } } - if (query.TopParentIds.Length > 1) + else if (queryTopParentIds.Length > 1) { - var val = string.Join(",", query.TopParentIds.Select(i => "'" + i + "'").ToArray()); + var val = string.Join(",", queryTopParentIds.Select(i => "'" + i + "'").ToArray()); if (enableItemsByName) { @@ -4544,7 +4581,7 @@ namespace Emby.Server.Implementations.Data return result; } - return new[] { value }; + return new[] { value }.Where(IsValidType); } public async Task DeleteItem(Guid id, CancellationToken cancellationToken) @@ -4696,31 +4733,35 @@ namespace Emby.Server.Implementations.Data statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidParamValue()); } } - if (query.PersonTypes.Count == 1) + var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); + + if (queryPersonTypes.Count == 1) { whereClauses.Add("PersonType=@PersonType"); if (statement != null) { - statement.TryBind("@PersonType", query.PersonTypes[0]); + statement.TryBind("@PersonType", queryPersonTypes[0]); } } - if (query.PersonTypes.Count > 1) + else if (queryPersonTypes.Count > 1) { - var val = string.Join(",", query.PersonTypes.Select(i => "'" + i + "'").ToArray()); + var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add("PersonType in (" + val + ")"); } - if (query.ExcludePersonTypes.Count == 1) + var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList(); + + if (queryExcludePersonTypes.Count == 1) { whereClauses.Add("PersonType<>@PersonType"); if (statement != null) { - statement.TryBind("@PersonType", query.ExcludePersonTypes[0]); + statement.TryBind("@PersonType", queryExcludePersonTypes[0]); } } - if (query.ExcludePersonTypes.Count > 1) + else if (queryExcludePersonTypes.Count > 1) { - var val = string.Join(",", query.ExcludePersonTypes.Select(i => "'" + i + "'").ToArray()); + var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add("PersonType not in (" + val + ")"); } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 147abd171..d477008a5 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Dto } } - //if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess)) + if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess)) { dto.PlayAccess = item.GetPlayAccess(user); } @@ -1639,7 +1639,7 @@ namespace Emby.Server.Implementations.Dto var width = size.Width; var height = size.Height; - if (width == 0 || height == 0) + if (width.Equals(0) || height.Equals(0)) { return null; } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c704d0e4e..670acd37f 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -84,7 +84,7 @@ - + @@ -102,7 +102,6 @@ - @@ -170,7 +169,6 @@ - @@ -302,8 +300,8 @@ ..\packages\Emby.XmlTv.1.0.7\lib\portable-net45+win8\Emby.XmlTv.dll True - - ..\packages\MediaBrowser.Naming.1.0.4\lib\portable-net45+win8\MediaBrowser.Naming.dll + + ..\packages\MediaBrowser.Naming.1.0.5\lib\portable-net45+win8\MediaBrowser.Naming.dll True diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index f841b8b6b..0a9c67285 100644 --- a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -677,20 +677,7 @@ namespace Emby.Server.Implementations.FileOrganization var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options); - // MAX_PATH - trailing charachter - drive component: 260 - 1 - 3 = 256 - // Usually newPath would include the drive component, but use 256 to be sure - var maxFilenameLength = 256 - newPath.Length; - - if (!newPath.EndsWith(@"\")) - { - // Remove 1 for missing backslash combining path and filename - maxFilenameLength--; - } - - // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt) - maxFilenameLength -= 4; - - var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength); + var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options); if (string.IsNullOrEmpty(episodeFileName)) { @@ -742,7 +729,7 @@ namespace Emby.Server.Implementations.FileOrganization return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); } - private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength) + private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); @@ -786,32 +773,15 @@ namespace Emby.Server.Implementations.FileOrganization .Replace("%0e", episodeNumber.ToString("00", _usCulture)) .Replace("%00e", episodeNumber.ToString("000", _usCulture)); - if (maxLength.HasValue && result.Contains("%#")) + if (result.Contains("%#")) { - // Substract 3 for the temp token length (%#1, %#2 or %#3) - int maxRemainingTitleLength = maxLength.Value - result.Length + 3; - string shortenedEpisodeTitle = string.Empty; - - if (maxRemainingTitleLength > 5) - { - // A title with fewer than 5 letters wouldn't be of much value - shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length)); - } - - result = result.Replace("%#1", shortenedEpisodeTitle) - .Replace("%#2", shortenedEpisodeTitle.Replace(" ", ".")) - .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_")); + result = result.Replace("%#1", episodeTitle) + .Replace("%#2", episodeTitle.Replace(" ", ".")) + .Replace("%#3", episodeTitle.Replace(" ", "_")); } - if (maxLength.HasValue && result.Length > maxLength.Value) - { - // There may be cases where reducing the title length may still not be sufficient to - // stay below maxLength - var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength); - throw new Exception(msg); - } - - return result; + // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters + return _fileSystem.GetValidFilename(result).Trim(); } private bool IsSameEpisode(string sourcePath, string newPath) diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs new file mode 100644 index 000000000..dbaf97b1e --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public class FileWriter : IHttpResult + { + private ILogger Logger { get; set; } + + private string RangeHeader { get; set; } + private bool IsHeadRequest { get; set; } + + private long RangeStart { get; set; } + private long RangeEnd { get; set; } + private long RangeLength { get; set; } + private long TotalContentLength { get; set; } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + public List Cookies { get; private set; } + + public FileShareMode FileShare { get; set; } + + /// + /// The _options + /// + private readonly IDictionary _options = new Dictionary(); + /// + /// Gets the options. + /// + /// The options. + public IDictionary Headers + { + get { return _options; } + } + + public string Path { get; set; } + + public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + Path = path; + Logger = logger; + RangeHeader = rangeHeader; + + Headers["Content-Type"] = contentType; + + TotalContentLength = fileSystem.GetFileInfo(path).Length; + + if (string.IsNullOrWhiteSpace(rangeHeader)) + { + Headers["Content-Length"] = TotalContentLength.ToString(UsCulture); + StatusCode = HttpStatusCode.OK; + } + else + { + Headers["Accept-Ranges"] = "bytes"; + StatusCode = HttpStatusCode.PartialContent; + SetRangeValues(); + } + + FileShare = FileShareMode.Read; + Cookies = new List(); + } + + /// + /// Sets the range values. + /// + private void SetRangeValues() + { + var requestedRange = RequestedRanges[0]; + + // If the requested range is "0-", we can optimize by just doing a stream copy + if (!requestedRange.Value.HasValue) + { + RangeEnd = TotalContentLength - 1; + } + else + { + RangeEnd = requestedRange.Value.Value; + } + + RangeStart = requestedRange.Key; + RangeLength = 1 + RangeEnd - RangeStart; + + // Content-Length is the length of what we're serving, not the original content + Headers["Content-Length"] = RangeLength.ToString(UsCulture); + Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); + } + + /// + /// The _requested ranges + /// + private List> _requestedRanges; + /// + /// Gets the requested ranges. + /// + /// The requested ranges. + protected List> RequestedRanges + { + get + { + if (_requestedRanges == null) + { + _requestedRanges = new List>(); + + // Example: bytes=0-,32-63 + var ranges = RangeHeader.Split('=')[1].Split(','); + + foreach (var range in ranges) + { + var vals = range.Split('-'); + + long start = 0; + long? end = null; + + if (!string.IsNullOrEmpty(vals[0])) + { + start = long.Parse(vals[0], UsCulture); + } + if (!string.IsNullOrEmpty(vals[1])) + { + end = long.Parse(vals[1], UsCulture); + } + + _requestedRanges.Add(new KeyValuePair(start, end)); + } + } + + return _requestedRanges; + } + } + + public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken) + { + try + { + // Headers only + if (IsHeadRequest) + { + return; + } + + if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1)) + { + Logger.Info("Transmit file {0}", Path); + await response.TransmitFile(Path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false); + return; + } + + await response.TransmitFile(Path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false); + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } + } + + public string ContentType { get; set; } + + public IRequest RequestContext { get; set; } + + public object Response { get; set; } + + public int Status { get; set; } + + public HttpStatusCode StatusCode + { + get { return (HttpStatusCode)Status; } + set { Status = (int)value; } + } + + public string StatusDescription { get; set; } + + } +} diff --git a/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs b/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs deleted file mode 100644 index 819ede1ab..000000000 --- a/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.HttpServer -{ - /// - /// Class GetDashboardResource - /// - [Route("/swagger-ui/{ResourceName*}", "GET")] - public class GetSwaggerResource - { - /// - /// Gets or sets the name. - /// - /// The name. - public string ResourceName { get; set; } - } -} \ No newline at end of file diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index b5a3c2992..6d15cc619 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -52,6 +52,7 @@ namespace Emby.Server.Implementations.HttpServer private readonly ISocketFactory _socketFactory; private readonly ICryptoProvider _cryptoProvider; + private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IXmlSerializer _xmlSerializer; private readonly ICertificate _certificate; @@ -70,8 +71,7 @@ namespace Emby.Server.Implementations.HttpServer ILogger logger, IServerConfigurationManager config, string serviceName, - string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn, bool enableDualModeSockets) - : base() + string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn, bool enableDualModeSockets, IFileSystem fileSystem) { Instance = this; @@ -89,6 +89,7 @@ namespace Emby.Server.Implementations.HttpServer _streamFactory = streamFactory; _funcParseFn = funcParseFn; _enableDualModeSockets = enableDualModeSockets; + _fileSystem = fileSystem; _config = config; _logger = logger; @@ -226,7 +227,8 @@ namespace Emby.Server.Implementations.HttpServer _cryptoProvider, _streamFactory, _enableDualModeSockets, - GetRequest); + GetRequest, + _fileSystem); } private IHttpRequest GetRequest(HttpListenerContext httpContext) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 6bfd83110..310161d41 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -474,10 +474,6 @@ namespace Emby.Server.Implementations.HttpServer { throw new ArgumentNullException("cacheKey"); } - if (options.ContentFactory == null) - { - throw new ArgumentNullException("factoryFn"); - } var key = cacheKey.ToString("N"); @@ -560,30 +556,44 @@ namespace Emby.Server.Implementations.HttpServer { var rangeHeader = requestContext.Headers.Get("Range"); - var stream = await factoryFn().ConfigureAwait(false); + if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path)) + { + return new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem) + { + OnComplete = options.OnComplete, + OnError = options.OnError, + FileShare = options.FileShare + }; + } if (!string.IsNullOrEmpty(rangeHeader)) { + var stream = await factoryFn().ConfigureAwait(false); + return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger) { OnComplete = options.OnComplete }; } - - responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); - - if (isHeadRequest) + else { - stream.Dispose(); + var stream = await factoryFn().ConfigureAwait(false); - return GetHttpResult(new byte[] { }, contentType, true); + responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture); + + if (isHeadRequest) + { + stream.Dispose(); + + return GetHttpResult(new byte[] { }, contentType, true); + } + + return new StreamWriter(stream, contentType, _logger) + { + OnComplete = options.OnComplete, + OnError = options.OnError + }; } - - return new StreamWriter(stream, contentType, _logger) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; } using (var stream = await factoryFn().ConfigureAwait(false)) diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index 652fc4f83..b11b2fe88 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -27,10 +27,11 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp private readonly ISocketFactory _socketFactory; private readonly ICryptoProvider _cryptoProvider; private readonly IStreamFactory _streamFactory; + private readonly IFileSystem _fileSystem; private readonly Func _httpRequestFactory; private readonly bool _enableDualMode; - public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func httpRequestFactory) + public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func httpRequestFactory, IFileSystem fileSystem) { _logger = logger; _certificate = certificate; @@ -42,6 +43,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp _streamFactory = streamFactory; _enableDualMode = enableDualMode; _httpRequestFactory = httpRequestFactory; + _fileSystem = fileSystem; } public Action ErrorHandler { get; set; } @@ -54,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp public void Start(IEnumerable urlPrefixes) { if (_listener == null) - _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); + _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider, _fileSystem); _listener.EnableDualMode = _enableDualMode; diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index 36f795411..fd30b227f 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using SocketHttpListener.Net; using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; @@ -189,5 +192,10 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp public void ClearCookies() { } + + public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken); + } } } diff --git a/Emby.Server.Implementations/HttpServer/SwaggerService.cs b/Emby.Server.Implementations/HttpServer/SwaggerService.cs deleted file mode 100644 index d41946645..000000000 --- a/Emby.Server.Implementations/HttpServer/SwaggerService.cs +++ /dev/null @@ -1,47 +0,0 @@ -using MediaBrowser.Controller; -using MediaBrowser.Controller.Net; -using System.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.HttpServer -{ - public class SwaggerService : IService, IRequiresRequest - { - private readonly IServerApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - - public SwaggerService(IServerApplicationPaths appPaths, IFileSystem fileSystem, IHttpResultFactory resultFactory) - { - _appPaths = appPaths; - _fileSystem = fileSystem; - _resultFactory = resultFactory; - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetSwaggerResource request) - { - var swaggerDirectory = Path.Combine(_appPaths.ApplicationResourcesPath, "swagger-ui"); - - var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', _fileSystem.DirectorySeparatorChar)); - - return _resultFactory.GetStaticFileResult(Request, requestedFile).Result; - } - - /// - /// Gets or sets the result factory. - /// - /// The result factory. - private readonly IHttpResultFactory _resultFactory; - - /// - /// Gets or sets the request context. - /// - /// The request context. - public IRequest Request { get; set; } - } -} diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 7a36691df..38908c2bd 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -258,7 +258,7 @@ namespace Emby.Server.Implementations.Images { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } - if (item is Playlist || item is MusicGenre) + if (item is Playlist || item is MusicGenre || item is Genre || item is GameGenre) { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f7706db47..026486efc 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -513,6 +513,11 @@ namespace Emby.Server.Implementations.Library } public Guid GetNewItemId(string key, Type type) + { + return GetNewItemIdInternal(key, type, false); + } + + private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive) { if (string.IsNullOrWhiteSpace(key)) { @@ -531,7 +536,7 @@ namespace Emby.Server.Implementations.Library .Replace("/", "\\"); } - if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) + if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds) { key = key.ToLower(); } @@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Library /// Task{Person}. public Person GetPerson(string name) { - return CreateItemByName(Person.GetPath(name), name); + return CreateItemByName(Person.GetPath, name); } /// @@ -875,7 +880,7 @@ namespace Emby.Server.Implementations.Library /// Task{Studio}. public Studio GetStudio(string name) { - return CreateItemByName(Studio.GetPath(name), name); + return CreateItemByName(Studio.GetPath, name); } /// @@ -885,7 +890,7 @@ namespace Emby.Server.Implementations.Library /// Task{Genre}. public Genre GetGenre(string name) { - return CreateItemByName(Genre.GetPath(name), name); + return CreateItemByName(Genre.GetPath, name); } /// @@ -895,7 +900,7 @@ namespace Emby.Server.Implementations.Library /// Task{MusicGenre}. public MusicGenre GetMusicGenre(string name) { - return CreateItemByName(MusicGenre.GetPath(name), name); + return CreateItemByName(MusicGenre.GetPath, name); } /// @@ -905,7 +910,7 @@ namespace Emby.Server.Implementations.Library /// Task{GameGenre}. public GameGenre GetGameGenre(string name) { - return CreateItemByName(GameGenre.GetPath(name), name); + return CreateItemByName(GameGenre.GetPath, name); } /// @@ -923,7 +928,7 @@ namespace Emby.Server.Implementations.Library var name = value.ToString(CultureInfo.InvariantCulture); - return CreateItemByName(Year.GetPath(name), name); + return CreateItemByName(Year.GetPath, name); } /// @@ -933,10 +938,10 @@ namespace Emby.Server.Implementations.Library /// Task{Genre}. public MusicArtist GetArtist(string name) { - return CreateItemByName(MusicArtist.GetPath(name), name); + return CreateItemByName(MusicArtist.GetPath, name); } - private T CreateItemByName(string path, string name) + private T CreateItemByName(Func getPathFn, string name) where T : BaseItem, new() { if (typeof(T) == typeof(MusicArtist)) @@ -957,7 +962,9 @@ namespace Emby.Server.Implementations.Library } } - var id = GetNewItemId(path, typeof(T)); + var path = getPathFn(name); + var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds; + var id = GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); var item = GetItemById(id) as T; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c1bd8fe91..ccd4c3631 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -369,6 +369,8 @@ namespace Emby.Server.Implementations.Library { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + enableAutoClose = false; + try { var tuple = GetProvider(request.OpenToken); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 926d82f94..0ea1b38b1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -442,6 +442,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false); + foreach (var channel in result) + { + _logger.Info("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id); + } + _epgChannels.AddOrUpdate(info.Id, result, (k, v) => result); } @@ -493,11 +498,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId)) { - var mappedTunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings); + var tunerChannelId = tunerChannel.TunerChannelId; + if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1) + { + tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I'); + } + + var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings); if (string.IsNullOrWhiteSpace(mappedTunerChannelId)) { - mappedTunerChannelId = tunerChannel.TunerChannelId; + mappedTunerChannelId = tunerChannelId; } var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase)); @@ -639,8 +650,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public Task CreateTimer(TimerInfo timer, CancellationToken cancellationToken) { - var existingTimer = _timerProvider.GetAll() - .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase)); + var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ? + null : + _timerProvider.GetTimerByProgramId(timer.ProgramId); if (existingTimer != null) { @@ -710,7 +722,34 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new InvalidOperationException("SeriesId for program not found"); } + // If any timers have already been manually created, make sure they don't get cancelled + var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false)) + .Where(i => + { + if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId)) + { + return true; + } + + if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId)) + { + return true; + } + + return false; + }) + .ToList(); + _seriesTimerProvider.Add(info); + + foreach (var timer in existingTimers) + { + timer.SeriesTimerId = info.Id; + timer.IsManual = true; + + _timerProvider.AddOrUpdate(timer, false); + } + await UpdateTimersForSeriesTimer(epgData, info, true, false).ConfigureAwait(false); return info.Id; @@ -991,6 +1030,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (epgChannel == null) { + _logger.Debug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); programs = new List(); } else @@ -1276,6 +1316,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } + var registration = await _liveTvManager.GetRegistrationInfo("dvr").ConfigureAwait(false); + if (!registration.IsValid) + { + _logger.Warn("Emby Premiere required to use Emby DVR."); + OnTimerOutOfDate(timer); + return; + } + var activeRecordingInfo = new ActiveRecordingInfo { CancellationTokenSource = new CancellationTokenSource(), @@ -2299,6 +2347,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var existingTimer = _timerProvider.GetTimer(timer.Id); + if (existingTimer == null) + { + existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) + ? null + : _timerProvider.GetTimerByProgramId(timer.ProgramId); + } + if (existingTimer == null) { if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) @@ -2313,12 +2368,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - // Only update if not currently active + // Only update if not currently active - test both new timer and existing in case Id's are different + // Id's could be different if the timer was created manually prior to series timer creation ActiveRecordingInfo activeRecordingInfo; - if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo)) + if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo) && !_activeRecordings.TryGetValue(existingTimer.Id, out activeRecordingInfo)) { UpdateExistingTimerWithNewMetadata(existingTimer, timer); + // Needed by ShouldCancelTimerForSeriesTimer + timer.IsManual = existingTimer.IsManual; + if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) { existingTimer.Status = RecordingStatus.Cancelled; @@ -2516,7 +2575,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV list.Add(new VirtualFolderInfo { Locations = new List { customPath }, - Name = "Recorded Series", + Name = "Recorded Shows", CollectionType = CollectionType.TvShows }); } @@ -2531,6 +2590,86 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public ProgramInfo Program { get; set; } public CancellationTokenSource CancellationTokenSource { get; set; } } + + private const int TunerDiscoveryDurationMs = 3000; + + public async Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) + { + var list = new List(); + + var configuredDeviceIds = GetConfiguration().TunerHosts + .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId)) + .Select(i => i.DeviceId) + .ToList(); + + foreach (var host in _liveTvManager.TunerHosts) + { + var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); + + if (newDevicesOnly) + { + discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase)) + .ToList(); + } + list.AddRange(discoveredDevices); + } + + return list; + } + + public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken) + { + foreach (var host in _liveTvManager.TunerHosts) + { + await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false); + } + } + + private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken) + { + var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); + + var configuredDevices = GetConfiguration().TunerHosts + .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var device in discoveredDevices) + { + var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)); + + if (configuredDevice != null) + { + if (!string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) + { + _logger.Info("Tuner url has changed from {0} to {1}", configuredDevice.Url, device.Url); + + configuredDevice.Url = device.Url; + await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false); + } + } + } + } + + private async Task> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken) + { + try + { + var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false); + + foreach (var device in discoveredDevices) + { + _logger.Info("Discovered tuner device {0} at {1}", host.Name, device.Url); + } + + return discoveredDevices; + } + catch (Exception ex) + { + _logger.ErrorException("Error discovering tuner devices", ex); + + return new List(); + } + } } public static class ConfigurationExtension { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 0567bdfd9..6cc5b6920 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks); var inputModifiers = "-fflags +genpts -async 1 -vsync -1"; - var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4} -y \"{1}\""; + var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4}{6} -y \"{1}\""; long startTimeTicks = 0; //if (mediaSource.DateLiveStreamOpened.HasValue) @@ -193,7 +193,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn"; - commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam); + var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ? + " -f mp4 -movflags frag_keyframe+empty_moov" : + string.Empty; + + commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam, outputParam); return inputModifiers + " " + commandLineArgs; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 35868d318..2eec3df8a 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -166,5 +166,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); } + + public TimerInfo GetTimerByProgramId(string programId) + { + return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index c22bb1171..21c4006a6 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -205,6 +205,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings } programInfo.ShowId = uniqueString.GetMD5().ToString("N"); + + // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped + if (programInfo.IsSeries && !programInfo.IsRepeat) + { + if ((programInfo.EpisodeNumber ?? 0) == 0) + { + programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); + } + } } // Construct an id from the channel and start date diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 618cd1d45..b9e73b62e 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -150,6 +150,21 @@ namespace Emby.Server.Implementations.LiveTv get { return _listingProviders; } } + public List GetTunerHostTypes() + { + return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair + { + Name = i.Name, + Id = i.Type + + }).ToList(); + } + + public Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) + { + return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken); + } + void service_DataSourceChanged(object sender, EventArgs e) { if (!_isDisposed) @@ -1063,25 +1078,28 @@ namespace Emby.Server.Implementations.LiveTv var channel = GetInternalChannel(program.ChannelId); - var channelUserdata = _userDataManager.GetUserData(userId, channel); + if (channel != null) + { + var channelUserdata = _userDataManager.GetUserData(userId, channel); - if (channelUserdata.Likes ?? false) - { - score += 2; - } - else if (!(channelUserdata.Likes ?? true)) - { - score -= 2; - } + if (channelUserdata.Likes ?? false) + { + score += 2; + } + else if (!(channelUserdata.Likes ?? true)) + { + score -= 2; + } - if (channelUserdata.IsFavorite) - { - score += 3; - } + if (channelUserdata.IsFavorite) + { + score += 3; + } - if (factorChannelWatchCount) - { - score += channelUserdata.PlayCount; + if (factorChannelWatchCount) + { + score += channelUserdata.PlayCount; + } } return score; @@ -1180,6 +1198,8 @@ namespace Emby.Server.Implementations.LiveTv { EmbyTV.EmbyTV.Current.CreateRecordingFolders(); + await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); + var numComplete = 0; double progressPerService = _services.Count == 0 ? 0 @@ -2748,7 +2768,7 @@ namespace Emby.Server.Implementations.LiveTv private bool IsLiveTvEnabled(User user) { - return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count(i => i.IsEnabled) > 0); + return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count > 0); } public IEnumerable GetEnabledUsers() @@ -2986,7 +3006,7 @@ namespace Emby.Server.Implementations.LiveTv if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase)) { var config = GetConfiguration(); - if (config.TunerHosts.Count(i => i.IsEnabled) > 0 && + if (config.TunerHosts.Count > 0 && config.ListingProviders.Count(i => (i.EnableAllTuners || i.EnabledTuners.Length > 0) && string.Equals(i.Type, SchedulesDirect.TypeName, StringComparison.OrdinalIgnoreCase)) > 0) { return Task.FromResult(new MBRegistrationRecord @@ -3000,50 +3020,6 @@ namespace Emby.Server.Implementations.LiveTv return _security.GetRegistrationStatus(feature); } - public List GetSatIniMappings() - { - return new List(); - //var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList(); - - //return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList(); - } - - public NameValuePair GetSatIniMappings(string resource) - { - return new NameValuePair(); - //using (var stream = GetType().Assembly.GetManifestResourceStream(resource)) - //{ - // using (var reader = new StreamReader(stream)) - // { - // var parser = new StreamIniDataParser(); - // IniData data = parser.ReadData(reader); - - // var satType1 = data["SATTYPE"]["1"]; - // var satType2 = data["SATTYPE"]["2"]; - - // if (string.IsNullOrWhiteSpace(satType2)) - // { - // return null; - // } - - // var srch = "SatIp.ini."; - // var filename = Path.GetFileName(resource); - - // return new NameValuePair - // { - // Name = satType1 + " " + satType2, - // Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length) - // }; - // } - //} - } - - public Task> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken) - { - return Task.FromResult(new List()); - //return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken); - } - public Task> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken) { var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index f2806292d..5582d8f35 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv public bool IsHidden { - get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count(i => i.IsEnabled) == 0; } + get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count == 0; } } public bool IsEnabled diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 5ac3812b0..74b8a7764 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); var list = result.ToList(); - Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); + Logger.Info("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); if (!string.IsNullOrWhiteSpace(key) && list.Count > 0) { @@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts protected virtual List GetTunerHosts() { return GetConfiguration().TunerHosts - .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -135,8 +135,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { // Check to make sure the tuner is available // If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error - if (hostsWithChannel.Count > 1 && - !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) + if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false)) { Logger.Error("Tuner is not currently available"); continue; @@ -208,6 +207,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts foreach (var host in hostsWithChannel) { + if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + try { var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); @@ -243,7 +247,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts protected abstract Task IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); - protected abstract bool IsValidChannelId(string channelId); + protected virtual string ChannelIdPrefix + { + get + { + return Type + "_"; + } + } + protected virtual bool IsValidChannelId(string channelId) + { + if (string.IsNullOrWhiteSpace(channelId)) + { + throw new ArgumentNullException("channelId"); + } + + return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); + } protected LiveTvOptions GetConfiguration() { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs deleted file mode 100644 index 336469c50..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs +++ /dev/null @@ -1,158 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Logging; -using System; -using System.Linq; -using System.Threading; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Serialization; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public class HdHomerunDiscovery : IServerEntryPoint - { - private readonly IDeviceDiscovery _deviceDiscovery; - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILiveTvManager _liveTvManager; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _json; - - public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json) - { - _deviceDiscovery = deviceDiscovery; - _config = config; - _logger = logger; - _liveTvManager = liveTvManager; - _httpClient = httpClient; - _json = json; - } - - public void Run() - { - _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; - } - - void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs e) - { - string server = null; - var info = e.Argument; - - if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) - { - string location; - if (info.Headers.TryGetValue("Location", out location)) - { - //_logger.Debug("HdHomerun found at {0}", location); - - // Just get the beginning of the url - Uri uri; - if (Uri.TryCreate(location, UriKind.Absolute, out uri)) - { - var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase) - .TrimEnd('/'); - - //_logger.Debug("HdHomerun api url: {0}", apiUrl); - AddDevice(apiUrl); - } - } - } - } - - private async void AddDevice(string url) - { - await _semaphore.WaitAsync().ConfigureAwait(false); - - try - { - var options = GetConfiguration(); - - if (options.TunerHosts.Any(i => - string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) && - UriEquals(i.Url, url))) - { - return; - } - - // Strip off the port - url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/'); - - // Test it by pulling down the lineup - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = string.Format("{0}/discover.json", url), - CancellationToken = CancellationToken.None, - BufferContent = false - })) - { - var response = _json.DeserializeFromStream(stream); - - var existing = GetConfiguration().TunerHosts - .FirstOrDefault(i => string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.DeviceId, response.DeviceID, StringComparison.OrdinalIgnoreCase)); - - if (existing == null) - { - await _liveTvManager.SaveTunerHost(new TunerHostInfo - { - Type = HdHomerunHost.DeviceType, - Url = url, - DeviceId = response.DeviceID - - }).ConfigureAwait(false); - } - else - { - if (!string.Equals(existing.Url, url, StringComparison.OrdinalIgnoreCase)) - { - existing.Url = url; - await _liveTvManager.SaveTunerHost(existing).ConfigureAwait(false); - } - } - } - } - catch (Exception ex) - { - _logger.ErrorException("Error saving device", ex); - } - finally - { - _semaphore.Release(); - } - } - - private bool UriEquals(string savedUri, string location) - { - return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase); - } - - private string NormalizeUrl(string url) - { - if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - url = "http://" + url; - } - - url = url.TrimEnd('/'); - - // Strip off the port - return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped); - } - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration("livetv"); - } - - public void Dispose() - { - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 62385e172..8fa1bbe23 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -30,8 +31,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; + private readonly IEnvironmentInfo _environment; - public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager) + public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; @@ -39,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; + _environment = environment; } public string Name @@ -56,7 +59,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun get { return "hdhomerun"; } } - private const string ChannelIdPrefix = "hdhr_"; + protected override string ChannelIdPrefix + { + get + { + return "hdhr_"; + } + } private string GetChannelId(TunerHostInfo info, Channels i) { @@ -125,7 +134,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun DiscoverResponse response; if (_modelCache.TryGetValue(info.Url, out response)) { - return response; + if ((DateTime.UtcNow - response.DateQueried).TotalHours <= 12) + { + return response; + } } } @@ -135,8 +147,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), CancellationToken = cancellationToken, - CacheLength = TimeSpan.FromDays(1), - CacheMode = CacheMode.Unconditional, TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), BufferContent = false @@ -215,7 +225,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var list = new List(); foreach (var host in GetConfiguration().TunerHosts - .Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))) + .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))) { try { @@ -268,6 +278,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public int HD { get; set; } } + protected EncodingOptions GetEncodingOptions() + { + return Config.GetConfiguration("encoding"); + } + + private string GetHdHrIdFromChannelId(string channelId) + { + return channelId.Split('_')[1]; + } + private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile) { int? width = null; @@ -355,14 +375,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun nal = "0"; } - var url = GetApiUrl(info, true) + "/auto/v" + channelId; - - // If raw was used, the tuner doesn't support params - if (!string.IsNullOrWhiteSpace(profile) - && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) - { - url += "?transcode=" + profile; - } + var url = GetApiUrl(info, false); var id = profile; if (string.IsNullOrWhiteSpace(id)) @@ -371,92 +384,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } id += "_" + url.GetMD5().ToString("N"); - var mediaSource = new MediaSourceInfo - { - Path = url, - Protocol = MediaProtocol.Http, - MediaStreams = new List - { - new MediaStream - { - Type = MediaStreamType.Video, - // Set the index to -1 because we don't know the exact index of the video stream within the container - Index = -1, - IsInterlaced = isInterlaced, - Codec = videoCodec, - Width = width, - Height = height, - BitRate = videoBitrate, - NalLengthSize = nal - - }, - new MediaStream - { - Type = MediaStreamType.Audio, - // Set the index to -1 because we don't know the exact index of the audio stream within the container - Index = -1, - Codec = audioCodec, - BitRate = audioBitrate - } - }, - RequiresOpening = true, - RequiresClosing = false, - BufferMs = 0, - Container = "ts", - Id = id, - SupportsDirectPlay = false, - SupportsDirectStream = true, - SupportsTranscoding = true, - IsInfiniteStream = true - }; - - mediaSource.InferTotalBitrate(); - - return mediaSource; - } - - protected EncodingOptions GetEncodingOptions() - { - return Config.GetConfiguration("encoding"); - } - - private string GetHdHrIdFromChannelId(string channelId) - { - return channelId.Split('_')[1]; - } - - private MediaSourceInfo GetLegacyMediaSource(TunerHostInfo info, string channelId, ChannelInfo channel) - { - int? width = null; - int? height = null; - bool isInterlaced = true; - string videoCodec = null; - string audioCodec = null; - - int? videoBitrate = null; - int? audioBitrate = null; - - if (channel != null) - { - if (string.IsNullOrWhiteSpace(videoCodec)) - { - videoCodec = channel.VideoCodec; - } - audioCodec = channel.AudioCodec; - } - - // normalize - if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase)) - { - videoCodec = "mpeg2video"; - } - - string nal = null; - - var url = GetApiUrl(info, false); - var id = channelId; - id += "_" + url.GetMD5().ToString("N"); - var mediaSource = new MediaSourceInfo { Path = url, @@ -520,7 +447,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (isLegacyTuner) { - list.Add(GetLegacyMediaSource(info, hdhrId, channelInfo)); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); } else { @@ -559,26 +486,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return list; } - protected override bool IsValidChannelId(string channelId) - { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException("channelId"); - } - - return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); - } - protected override async Task GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) { 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)) - { - throw new ArgumentException("Channel not found"); - } var hdhrId = GetHdHrIdFromChannelId(channelId); var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); @@ -586,30 +499,40 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var hdhomerunChannel = channelInfo as HdHomerunChannelInfo; - if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) - { - var mediaSource = GetLegacyMediaSource(info, hdhrId, channelInfo); - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) + { return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); } - else + + // The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet + var enableHttpStream = _environment.OperatingSystem == OperatingSystem.OSX || + _environment.OperatingSystem == OperatingSystem.BSD; + enableHttpStream = true; + if (enableHttpStream) { - var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); - //var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + mediaSource.Protocol = MediaProtocol.Http; + + var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId; + + // If raw was used, the tuner doesn't support params + if (!string.IsNullOrWhiteSpace(profile) + && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) + { + httpUrl += "?transcode=" + profile; + } + mediaSource.Path = httpUrl; return new HdHomerunHttpStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); - //return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); } + + return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); } public async Task Validate(TunerHostInfo info) { - if (!info.IsEnabled) - { - return; - } - lock (_modelCache) { _modelCache.Clear(); @@ -651,6 +574,83 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public string BaseURL { get; set; } public string LineupURL { get; set; } public int TunerCount { get; set; } + + public DateTime DateQueried { get; set; } + + public DiscoverResponse() + { + DateQueried = DateTime.UtcNow; + } + } + + public async Task> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) + { + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token; + var list = new List(); + + // Create udp broadcast discovery message + byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 }; + using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0)) + { + // Need a way to set the Receive timeout on the socket otherwise this might never timeout? + try + { + await udpClient.SendAsync(discBytes, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken); + while (!cancellationToken.IsCancellationRequested) + { + var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var deviceIp = response.RemoteEndPoint.IpAddress.Address; + + // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte + if (response.ReceivedBytes > 13 && response.Buffer[1] == 3) + { + var deviceAddress = "http://" + deviceIp; + + var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false); + + if (info != null) + { + list.Add(info); + } + } + } + + } + catch (OperationCanceledException) + { + } + catch + { + // Socket timeout indicates all messages have been received. + } + } + + return list; + } + + private async Task TryGetTunerHostInfo(string url, CancellationToken cancellationToken) + { + var hostInfo = new TunerHostInfo + { + Type = Type, + Url = url + }; + + try + { + var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); + + hostInfo.DeviceId = modelInfo.DeviceID; + hostInfo.FriendlyName = modelInfo.FriendlyName; + + return hostInfo; + } + catch + { + // logged at lower levels + } + + return null; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index a881d0ea1..e1572ea3f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -120,7 +120,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // send url to start streaming await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, cancellationToken).ConfigureAwait(false); - var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); _logger.Info("Opened HDHR UDP stream from {0}", remoteAddress); if (!cancellationToken.IsCancellationRequested) @@ -131,12 +130,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun onStarted = () => openTaskCompletionSource.TrySetResult(true); } - var stream = new UdpClientStream(udpClient); - await _multicastStream.CopyUntilCancelled(stream, onStarted, cancellationToken).ConfigureAwait(false); + await _multicastStream.CopyUntilCancelled(new UdpClientStream(udpClient), onStarted, cancellationToken).ConfigureAwait(false); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + _logger.Info("HDHR UDP stream cancelled or timed out from {0}", remoteAddress); + openTaskCompletionSource.TrySetException(ex); break; } catch (Exception ex) @@ -155,7 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } await hdHomerunManager.StopStreaming().ConfigureAwait(false); - udpClient.Dispose(); _liveStreamTaskCompletionSource.TrySetResult(true); } } @@ -207,7 +206,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var data = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); var bytesRead = data.ReceivedBytes - RtpHeaderBytes; - + // remove rtp header Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead); offset += bytesRead; @@ -291,4 +290,4 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun throw new NotImplementedException(); } } -} +} \ No newline at end of file diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index a939cec7b..4ec70f802 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts get { return "M3U Tuner"; } } - private const string ChannelIdPrefix = "m3u_"; - protected override async Task> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) { var result = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false); @@ -87,16 +85,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - protected override bool IsValidChannelId(string channelId) - { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException("channelId"); - } - - return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); - } - protected override async Task> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) { var urlHash = info.Url.GetMD5().ToString("N"); @@ -176,5 +164,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { return Task.FromResult(true); } + + public Task> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) + { + return Task.FromResult(new List()); + } } } \ No newline at end of file diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs index 90ff36441..281632590 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.LiveTv.TunerHosts { @@ -34,13 +35,73 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (bytesRead > 0) { - byte[] copy = new byte[bytesRead]; - Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead); - var allStreams = _outputStreams.ToList(); - foreach (var stream in allStreams) + + if (allStreams.Count == 1) { - stream.Value.Queue(copy); + await allStreams[0].Value.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); + } + else + { + byte[] copy = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead); + + foreach (var stream in allStreams) + { + stream.Value.Queue(copy, 0, copy.Length); + } + } + + if (onStarted != null) + { + var onStartedCopy = onStarted; + onStarted = null; + Task.Run(onStartedCopy); + } + } + + else + { + await Task.Delay(100).ConfigureAwait(false); + } + } + } + + private static int RtpHeaderBytes = 12; + public async Task CopyUntilCancelled(ISocket udpClient, Action onStarted, CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + + while (!cancellationToken.IsCancellationRequested) + { + var receiveToken = cancellationToken; + + // On the first connection attempt, put a timeout to avoid being stuck indefinitely in the event of failure + if (onStarted != null) + { + receiveToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(5000).Token, cancellationToken).Token; + } + + var data = await udpClient.ReceiveAsync(receiveToken).ConfigureAwait(false); + var bytesRead = data.ReceivedBytes - RtpHeaderBytes; + + if (bytesRead > 0) + { + var allStreams = _outputStreams.ToList(); + + if (allStreams.Count == 1) + { + await allStreams[0].Value.WriteAsync(data.Buffer, 0, bytesRead).ConfigureAwait(false); + } + else + { + byte[] copy = new byte[bytesRead]; + Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, copy, 0, bytesRead); + + foreach (var stream in allStreams) + { + stream.Value.Queue(copy, 0, copy.Length); + } } if (onStarted != null) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs index 7b48ce21a..543d2e373 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs @@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public class QueueStream { private readonly Stream _outputStream; - private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + private readonly ConcurrentQueue> _queue = new ConcurrentQueue>(); private CancellationToken _cancellationToken; public TaskCompletionSource TaskCompletion { get; private set; } @@ -28,9 +28,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts TaskCompletion = new TaskCompletionSource(); } - public void Queue(byte[] bytes) + public void Queue(byte[] bytes, int offset, int count) { - _queue.Enqueue(bytes); + _queue.Enqueue(new Tuple(bytes, offset, count)); } public void Start(CancellationToken cancellationToken) @@ -39,17 +39,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Task.Run(() => StartInternal()); } - private byte[] Dequeue() + private Tuple Dequeue() { - byte[] bytes; - if (_queue.TryDequeue(out bytes)) + Tuple result; + if (_queue.TryDequeue(out result)) { - return bytes; + return result; } return null; } + private void OnClosed() + { + GC.Collect(); + if (OnFinished != null) + { + OnFinished(this); + } + } + + public async Task WriteAsync(byte[] bytes, int offset, int count) + { + //return _outputStream.WriteAsync(bytes, offset, count, cancellationToken); + var cancellationToken = _cancellationToken; + + try + { + await _outputStream.WriteAsync(bytes, offset, count, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + _logger.Debug("QueueStream cancelled"); + TaskCompletion.TrySetCanceled(); + OnClosed(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in QueueStream", ex); + TaskCompletion.TrySetException(ex); + OnClosed(); + } + } + private async Task StartInternal() { var cancellationToken = _cancellationToken; @@ -58,10 +90,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { while (true) { - var bytes = Dequeue(); - if (bytes != null) + var result = Dequeue(); + if (result != null) { - await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + await _outputStream.WriteAsync(result.Item1, result.Item2, result.Item3, cancellationToken).ConfigureAwait(false); } else { @@ -81,10 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } finally { - if (OnFinished != null) - { - OnFinished(this); - } + OnClosed(); } } } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 120f445c2..64a41acef 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -408,6 +408,7 @@ namespace Emby.Server.Implementations.Localization new LocalizatonOption{ Name="Italian", Value="it"}, new LocalizatonOption{ Name="Kazakh", Value="kk"}, new LocalizatonOption{ Name="Norwegian Bokmål", Value="nb"}, + new LocalizatonOption{ Name="Persian", Value="fa"}, new LocalizatonOption{ Name="Polish", Value="pl"}, new LocalizatonOption{ Name="Portuguese (Brazil)", Value="pt-BR"}, new LocalizatonOption{ Name="Portuguese (Portugal)", Value="pt-PT"}, diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index ef7d6dba8..9514c12ca 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using Emby.Server.Implementations.Images; using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Library; @@ -101,4 +102,35 @@ namespace Emby.Server.Implementations.Playlists //} } + public class GenreImageProvider : BaseDynamicImageProvider + { + private readonly ILibraryManager _libraryManager; + + public GenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _libraryManager = libraryManager; + } + + protected override Task> GetItemsWithImages(IHasImages item) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + Genres = new[] { item.Name }, + IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name }, + SortBy = new[] { ItemSortBy.Random }, + Limit = 4, + Recursive = true, + ImageTypes = new[] { ImageType.Primary } + + }).ToList(); + + return Task.FromResult(GetFinalItems(items)); + } + + //protected override Task CreateImage(IHasImages item, List itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + //{ + // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); + //} + } + } diff --git a/Emby.Server.Implementations/Security/PluginSecurityManager.cs b/Emby.Server.Implementations/Security/PluginSecurityManager.cs index a26df7625..d42fae3ad 100644 --- a/Emby.Server.Implementations/Security/PluginSecurityManager.cs +++ b/Emby.Server.Implementations/Security/PluginSecurityManager.cs @@ -301,10 +301,12 @@ namespace Emby.Server.Implementations.Security if (reg.registered) { + _logger.Info("Registered for feature {0}", feature); LicenseFile.AddRegCheck(feature, reg.expDate); } else { + _logger.Info("Not registered for feature {0}", feature); LicenseFile.RemoveRegCheck(feature); } diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index d4ce1cabf..3c2af60db 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -12,45 +12,6 @@ namespace Emby.Server.Implementations.Services { public static class ResponseHelper { - private static async Task WriteToOutputStream(IResponse response, object result) - { - var asyncStreamWriter = result as IAsyncStreamWriter; - if (asyncStreamWriter != null) - { - await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false); - return true; - } - - var streamWriter = result as IStreamWriter; - if (streamWriter != null) - { - streamWriter.WriteTo(response.OutputStream); - return true; - } - - var stream = result as Stream; - if (stream != null) - { - using (stream) - { - await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false); - return true; - } - } - - var bytes = result as byte[]; - if (bytes != null) - { - response.ContentType = "application/octet-stream"; - response.SetContentLength(bytes.Length); - - await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - return true; - } - - return false; - } - public static Task WriteToResponse(IResponse httpRes, IRequest httpReq, object result) { if (result == null) @@ -141,16 +102,51 @@ namespace Emby.Server.Implementations.Services response.ContentType += "; charset=utf-8"; } - var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false); - if (writeToOutputStreamResult) + var asyncStreamWriter = result as IAsyncStreamWriter; + if (asyncStreamWriter != null) { + await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false); + return; + } + + var streamWriter = result as IStreamWriter; + if (streamWriter != null) + { + streamWriter.WriteTo(response.OutputStream); + return; + } + + var fileWriter = result as FileWriter; + if (fileWriter != null) + { + await fileWriter.WriteToAsync(response, CancellationToken.None).ConfigureAwait(false); + return; + } + + var stream = result as Stream; + if (stream != null) + { + using (stream) + { + await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false); + return; + } + } + + var bytes = result as byte[]; + if (bytes != null) + { + response.ContentType = "application/octet-stream"; + response.SetContentLength(bytes.Length); + + await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); return; } var responseText = result as string; if (responseText != null) { - var bytes = Encoding.UTF8.GetBytes(responseText); + bytes = Encoding.UTF8.GetBytes(responseText); response.SetContentLength(bytes.Length); await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); return; @@ -163,7 +159,7 @@ namespace Emby.Server.Implementations.Services { var contentType = request.ResponseContentType; var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - + using (var ms = new MemoryStream()) { serializer(result, ms); diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 9dfaa102a..b5e64bc23 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.TV // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items); - return GetResult(episodes, null, request); + return GetResult(episodes, request); } public QueryResult GetNextUp(NextUpQuery request, List parentsFolders) @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.TV // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items); - return GetResult(episodes, null, request); + return GetResult(episodes, request); } public IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable seriesKeys) @@ -163,8 +163,7 @@ namespace Emby.Server.Implementations.TV return false; }) .Select(i => i.Item2()) - .Where(i => i != null) - .Take(request.Limit ?? int.MaxValue); + .Where(i => i != null); } private string GetUniqueSeriesKey(BaseItem series) @@ -232,24 +231,30 @@ namespace Emby.Server.Implementations.TV return new Tuple>(DateTime.MinValue, getEpisode); } - private QueryResult GetResult(IEnumerable items, int? totalRecordLimit, NextUpQuery query) + private QueryResult GetResult(IEnumerable items, NextUpQuery query) { - var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); - var totalCount = itemsArray.Length; + int totalCount = 0; + if (query.EnableTotalRecordCount) + { + var list = items.ToList(); + totalCount = list.Count; + items = list; + } + + if (query.StartIndex.HasValue) + { + items = items.Skip(query.StartIndex.Value); + } if (query.Limit.HasValue) { - itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); - } - else if (query.StartIndex.HasValue) - { - itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray(); + items = items.Take(query.Limit.Value); } return new QueryResult { TotalRecordCount = totalCount, - Items = itemsArray + Items = items.ToArray() }; } } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index bb303d8fa..21ef3cab6 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -203,19 +203,6 @@ namespace Emby.Server.Implementations.Udp GC.SuppressFinalize(this); } - /// - /// Stops this instance. - /// - public void Stop() - { - _isDisposed = true; - - if (_udpClient != null) - { - _udpClient.Dispose(); - } - } - /// /// Releases unmanaged and - optionally - managed resources. /// @@ -224,7 +211,12 @@ namespace Emby.Server.Implementations.Udp { if (dispose) { - Stop(); + _isDisposed = true; + + if (_udpClient != null) + { + _udpClient.Dispose(); + } } } @@ -247,9 +239,13 @@ namespace Emby.Server.Implementations.Udp try { - await _udpClient.SendAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false); + await _udpClient.SendWithLockAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false); _logger.Info("Udp message sent to {0}", remoteEndPoint); + } + catch (OperationCanceledException) + { + } catch (Exception ex) { diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config index 7e638e171..ccabbc27b 100644 --- a/Emby.Server.Implementations/packages.config +++ b/Emby.Server.Implementations/packages.config @@ -1,7 +1,7 @@  - + diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index dc4e57155..2d8744f70 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -582,13 +582,13 @@ namespace MediaBrowser.Api.LiveTv } [Route("/LiveTv/ListingProviders/Default", "GET")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class GetDefaultListingProvider : ListingsProviderInfo, IReturn { } [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class AddListingProvider : ListingsProviderInfo, IReturn { public bool ValidateLogin { get; set; } @@ -596,7 +596,7 @@ namespace MediaBrowser.Api.LiveTv } [Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class DeleteListingProvider : IReturnVoid { [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")] @@ -604,7 +604,7 @@ namespace MediaBrowser.Api.LiveTv } [Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class GetLineups : IReturn> { [ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] @@ -621,13 +621,13 @@ namespace MediaBrowser.Api.LiveTv } [Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class GetSchedulesDirectCountries { } [Route("/LiveTv/ChannelMappingOptions")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class GetChannelMappingOptions { [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] @@ -635,7 +635,7 @@ namespace MediaBrowser.Api.LiveTv } [Route("/LiveTv/ChannelMappings")] - [Authenticated(AllowBeforeStartupWizard = true)] + [Authenticated] public class SetChannelMapping { [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] @@ -660,20 +660,6 @@ namespace MediaBrowser.Api.LiveTv public string Feature { get; set; } } - [Route("/LiveTv/TunerHosts/Satip/IniMappings", "GET", Summary = "Gets available mappings")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetSatIniMappings : IReturn> - { - - } - - [Route("/LiveTv/TunerHosts/Satip/ChannelScan", "GET", Summary = "Scans for available channels")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetSatChannnelScanResult : TunerHostInfo - { - - } - [Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")] public class GetLiveStreamFile { @@ -687,6 +673,20 @@ namespace MediaBrowser.Api.LiveTv public string Id { get; set; } } + [Route("/LiveTv/TunerHosts/Types", "GET")] + [Authenticated] + public class GetTunerHostTypes : IReturn> + { + + } + + [Route("/LiveTv/Tuners/Discvover", "GET")] + [Authenticated] + public class DiscoverTuners : IReturn> + { + public bool NewDevicesOnly { get; set; } + } + public class LiveTvService : BaseApiService { private readonly ILiveTvManager _liveTvManager; @@ -712,6 +712,12 @@ namespace MediaBrowser.Api.LiveTv _sessionContext = sessionContext; } + public object Get(GetTunerHostTypes request) + { + var list = _liveTvManager.GetTunerHostTypes(); + return ToOptimizedResult(list); + } + public object Get(GetLiveRecordingFile request) { var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id); @@ -731,6 +737,12 @@ namespace MediaBrowser.Api.LiveTv }; } + public async Task Get(DiscoverTuners request) + { + var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false); + return ToOptimizedResult(result); + } + public async Task Get(GetLiveStreamFile request) { var directStreamProvider = (await _liveTvManager.GetEmbyTvLiveStream(request.Id).ConfigureAwait(false)) as IDirectStreamProvider; @@ -749,13 +761,6 @@ namespace MediaBrowser.Api.LiveTv return ToOptimizedResult(new ListingsProviderInfo()); } - public async Task Get(GetSatChannnelScanResult request) - { - var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false); - - return ToOptimizedResult(result); - } - public async Task Get(GetLiveTvRegistrationInfo request) { var result = await _liveTvManager.GetRegistrationInfo(request.Feature).ConfigureAwait(false); @@ -803,11 +808,6 @@ namespace MediaBrowser.Api.LiveTv return ToOptimizedResult(result); } - public object Get(GetSatIniMappings request) - { - return ToOptimizedResult(_liveTvManager.GetSatIniMappings()); - } - public async Task Get(GetSchedulesDirectCountries request) { // https://json.schedulesdirect.org/20141201/available/countries diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index e1559cabf..480508e6f 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -289,8 +289,10 @@ namespace MediaBrowser.Api.Playback // MUST read both stdout and stderr asynchronously or a deadlock may occurr //process.BeginOutputReadLine(); + state.TranscodingJob = transcodingJob; + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream)); + new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream); // Wait for the file to exist before proceeeding while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) @@ -340,134 +342,6 @@ namespace MediaBrowser.Api.Playback // string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase); } - private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) - { - try - { - using (var reader = new StreamReader(source)) - { - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - - ParseLogLine(line, transcodingJob, state); - - var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); - - await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - await target.FlushAsync().ConfigureAwait(false); - } - } - } - catch (ObjectDisposedException) - { - // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux - } - catch (Exception ex) - { - Logger.ErrorException("Error reading ffmpeg log", ex); - } - } - - private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state) - { - float? framerate = null; - double? percent = null; - TimeSpan? transcodingPosition = null; - long? bytesTranscoded = null; - int? bitRate = null; - - var parts = line.Split(' '); - - var totalMs = state.RunTimeTicks.HasValue - ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds - : 0; - - var startMs = state.Request.StartTimeTicks.HasValue - ? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds - : 0; - - for (var i = 0; i < parts.Length; i++) - { - var part = parts[i]; - - if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) && - (i + 1 < parts.Length)) - { - var rate = parts[i + 1]; - float val; - - if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val)) - { - framerate = val; - } - } - else if (state.RunTimeTicks.HasValue && - part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) - { - var time = part.Split(new[] { '=' }, 2).Last(); - TimeSpan val; - - if (TimeSpan.TryParse(time, UsCulture, out val)) - { - var currentMs = startMs + val.TotalMilliseconds; - - var percentVal = currentMs / totalMs; - percent = 100 * percentVal; - - transcodingPosition = val; - } - } - else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) - { - var size = part.Split(new[] { '=' }, 2).Last(); - - int? scale = null; - if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) - { - scale = 1024; - size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - if (scale.HasValue) - { - long val; - - if (long.TryParse(size, NumberStyles.Any, UsCulture, out val)) - { - bytesTranscoded = val * scale.Value; - } - } - } - else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) - { - var rate = part.Split(new[] { '=' }, 2).Last(); - - int? scale = null; - if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) - { - scale = 1024; - rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - if (scale.HasValue) - { - float val; - - if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val)) - { - bitRate = (int)Math.Ceiling(val * scale.Value); - } - } - } - } - - if (framerate.HasValue || percent.HasValue) - { - ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate); - } - } - /// /// Processes the exited. /// @@ -697,6 +571,20 @@ namespace MediaBrowser.Api.Playback { request.SubtitleCodec = val; } + else if (i == 31) + { + if (videoRequest != null) + { + videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + } + else if (i == 32) + { + if (videoRequest != null) + { + videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + } } } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index aaca1793c..53813860a 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -41,9 +41,16 @@ namespace MediaBrowser.Api.Playback.Hls /// /// Gets the segment file extension. /// - /// The state. - /// System.String. - protected abstract string GetSegmentFileExtension(StreamState state); + protected string GetSegmentFileExtension(StreamRequest request) + { + var segmentContainer = request.SegmentContainer; + if (!string.IsNullOrWhiteSpace(segmentContainer)) + { + return "." + segmentContainer; + } + + return ".ts"; + } /// /// Gets the type of the transcoding job. @@ -103,8 +110,11 @@ namespace MediaBrowser.Api.Playback.Hls throw; } - var waitForSegments = state.SegmentLength >= 10 ? 2 : 3; - await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false); + var minSegments = state.MinSegments; + if (minSegments > 0) + { + await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false); + } } } finally @@ -258,14 +268,22 @@ namespace MediaBrowser.Api.Playback.Hls "hls/" + Path.GetFileNameWithoutExtension(outputPath)); } - var useGenericSegmenter = false; + var useGenericSegmenter = true; if (useGenericSegmenter) { - var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state); + var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request); var timeDeltaParam = String.Empty; - return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.'); + if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) + { + segmentFormat = "mpegts"; + } + + baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath)); + + return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", inputModifier, EncodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -276,7 +294,9 @@ namespace MediaBrowser.Api.Playback.Hls startNumberParam, outputPath, outputTsArg, - timeDeltaParam + timeDeltaParam, + segmentFormat, + baseUrlParam ).Trim(); } @@ -286,7 +306,7 @@ namespace MediaBrowser.Api.Playback.Hls var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"", itsOffset, inputModifier, - EncodingHelper.GetInputArgument(state, encodingOptions), + EncodingHelper.GetInputArgument(state, encodingOptions), threads, EncodingHelper.GetMapArgs(state), GetVideoArguments(state), diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 1074a8bc1..f77a66f8e 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Api.Playback.Hls { } - [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")] + [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")] public class GetHlsVideoSegment : VideoStreamRequest { public string PlaylistId { get; set; } @@ -77,8 +77,7 @@ namespace MediaBrowser.Api.Playback.Hls public string SegmentId { get; set; } } - [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.aac", "GET")] - [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")] + [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")] public class GetHlsAudioSegment : StreamRequest { public string PlaylistId { get; set; } @@ -158,7 +157,7 @@ namespace MediaBrowser.Api.Playback.Hls var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex); - var segmentExtension = GetSegmentFileExtension(state); + var segmentExtension = GetSegmentFileExtension(state.Request); TranscodingJob job = null; @@ -420,7 +419,7 @@ namespace MediaBrowser.Api.Playback.Hls var filename = Path.GetFileNameWithoutExtension(playlist); - return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state)); + return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request)); } private async Task GetSegmentResult(StreamState state, string playlistPath, @@ -740,7 +739,7 @@ namespace MediaBrowser.Api.Playback.Hls name, index.ToString(UsCulture), - GetSegmentFileExtension(isOutputVideo), + GetSegmentFileExtension(request), queryString)); index++; @@ -848,7 +847,7 @@ namespace MediaBrowser.Api.Playback.Hls var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg; + args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg; //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; @@ -897,7 +896,7 @@ namespace MediaBrowser.Api.Playback.Hls if (useGenericSegmenter) { - var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state); + var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request); var timeDeltaParam = String.Empty; @@ -907,7 +906,13 @@ namespace MediaBrowser.Api.Playback.Hls timeDeltaParam = string.Format("-segment_time_delta -{0}", startTime); } - return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.'); + if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) + { + segmentFormat = "mpegts"; + } + + return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", inputModifier, EncodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -918,7 +923,8 @@ namespace MediaBrowser.Api.Playback.Hls startNumberParam, outputPath, outputTsArg, - timeDeltaParam + timeDeltaParam, + segmentFormat ).Trim(); } @@ -935,20 +941,5 @@ namespace MediaBrowser.Api.Playback.Hls outputPath ).Trim(); } - - /// - /// Gets the segment file extension. - /// - /// The state. - /// System.String. - protected override string GetSegmentFileExtension(StreamState state) - { - return GetSegmentFileExtension(state.IsOutputVideo); - } - - protected string GetSegmentFileExtension(bool isOutputVideo) - { - return isOutputVideo ? ".ts" : ".ts"; - } } } \ No newline at end of file diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index f3683c6cb..03291670b 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Hls /// /// Class GetHlsVideoSegment /// - [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] + [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")] public class GetHlsVideoSegmentLegacy : VideoStreamRequest { public string PlaylistId { get; set; } @@ -109,11 +109,13 @@ namespace MediaBrowser.Api.Playback.Hls public Task Get(GetHlsVideoSegmentLegacy request) { var file = request.SegmentId + Path.GetExtension(Request.PathInfo); - file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file); + + var transcodeFolderPath = _config.ApplicationPaths.TranscodingTempPath; + file = Path.Combine(transcodeFolderPath, file); var normalizedPlaylistId = request.PlaylistId; - var playlistPath = _fileSystem.GetFilePaths(_config.ApplicationPaths.TranscodingTempPath) + var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) .FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1); return GetFileResult(file, playlistPath); diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index c9c6acc1b..f9164c36f 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Api.Playback.Hls var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode; var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg; + args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg; // Add resolution params, if specified if (!hasGraphicalSubs) @@ -118,16 +118,6 @@ namespace MediaBrowser.Api.Playback.Hls return args; } - /// - /// Gets the segment file extension. - /// - /// The state. - /// System.String. - protected override string GetSegmentFileExtension(StreamState state) - { - return ".ts"; - } - public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext) { } diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index ed8449b83..4e4e8858e 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -127,7 +127,7 @@ namespace MediaBrowser.Api.Playback SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex, - request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true); + request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true, true); } else { @@ -169,7 +169,7 @@ namespace MediaBrowser.Api.Playback { var mediaSourceId = request.MediaSourceId; - SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.EnableDirectStream, request.EnableTranscoding); + SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.ForceDirectPlayRemoteMediaSource, request.EnableDirectStream, request.EnableTranscoding); } return info; @@ -253,6 +253,7 @@ namespace MediaBrowser.Api.Playback int? maxAudioChannels, string userId, bool enableDirectPlay, + bool forceDirectPlayRemoteMediaSource, bool enableDirectStream, bool enableTranscoding) { @@ -260,7 +261,7 @@ namespace MediaBrowser.Api.Playback foreach (var mediaSource in result.MediaSources) { - SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, enableDirectStream, enableTranscoding); + SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding); } SortMediaSources(result, maxBitrate); @@ -279,6 +280,7 @@ namespace MediaBrowser.Api.Playback string playSessionId, string userId, bool enableDirectPlay, + bool forceDirectPlayRemoteMediaSource, bool enableDirectStream, bool enableTranscoding) { @@ -318,43 +320,49 @@ namespace MediaBrowser.Api.Playback if (mediaSource.SupportsDirectPlay) { - var supportsDirectStream = mediaSource.SupportsDirectStream; - - // Dummy this up to fool StreamBuilder - mediaSource.SupportsDirectStream = true; - options.MaxBitrate = maxBitrate; - - if (item is Audio) + if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource) { - if (!user.Policy.EnableAudioPlaybackTranscoding) + } + else + { + var supportsDirectStream = mediaSource.SupportsDirectStream; + + // Dummy this up to fool StreamBuilder + mediaSource.SupportsDirectStream = true; + options.MaxBitrate = maxBitrate; + + if (item is Audio) { - options.ForceDirectPlay = true; + if (!user.Policy.EnableAudioPlaybackTranscoding) + { + options.ForceDirectPlay = true; + } } - } - else if (item is Video) - { - if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + else if (item is Video) { - options.ForceDirectPlay = true; + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + options.ForceDirectPlay = true; + } } - } - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? - streamBuilder.BuildAudioItem(options) : - streamBuilder.BuildVideoItem(options); + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? + streamBuilder.BuildAudioItem(options) : + streamBuilder.BuildVideoItem(options); - if (streamInfo == null || !streamInfo.IsDirectStream) - { - mediaSource.SupportsDirectPlay = false; - } + if (streamInfo == null || !streamInfo.IsDirectStream) + { + mediaSource.SupportsDirectPlay = false; + } - // Set this back to what it was - mediaSource.SupportsDirectStream = supportsDirectStream; + // Set this back to what it was + mediaSource.SupportsDirectStream = supportsDirectStream; - if (streamInfo != null) - { - SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + if (streamInfo != null) + { + SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); + } } } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 23a84e480..646a91c27 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -15,9 +15,6 @@ using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Playback.Progressive @@ -298,7 +295,8 @@ namespace MediaBrowser.Api.Playback.Progressive responseHeaders["Accept-Ranges"] = "none"; } - if (response.ContentLength.HasValue) + // Seeing cases of -1 here + if (response.ContentLength.HasValue && response.ContentLength.Value >= 0) { responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index bc600d3ea..8394e016c 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; @@ -7,15 +6,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; -using System; -using System.IO; -using System.Linq; using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Playback.Progressive @@ -100,181 +92,7 @@ namespace MediaBrowser.Api.Playback.Progressive { var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - // Get the output codec name - var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions); - - var format = string.Empty; - var keyFrame = string.Empty; - - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)) - { - // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js - format = " -f mp4 -movflags frag_keyframe+empty_moov"; - } - - var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)); - - var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); - - var subtitleArguments = state.SubtitleStream != null && - state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed - ? GetSubtitleArguments(state) - : string.Empty; - - return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", - inputModifier, - EncodingHelper.GetInputArgument(state, encodingOptions), - keyFrame, - EncodingHelper.GetMapArgs(state), - GetVideoArguments(state, videoCodec), - threads, - GetAudioArguments(state), - subtitleArguments, - format, - outputPath - ).Trim(); - } - - private string GetSubtitleArguments(StreamState state) - { - var format = state.SupportedSubtitleCodecs.FirstOrDefault(); - string codec; - - if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase)) - { - codec = "copy"; - } - else - { - codec = format; - } - - return " -codec:s:0 " + codec; - } - - /// - /// Gets video arguments to pass to ffmpeg - /// - /// The state. - /// The video codec. - /// System.String. - private string GetVideoArguments(StreamState state, string videoCodec) - { - var args = "-codec:v:0 " + videoCodec; - - if (state.EnableMpegtsM2TsMode) - { - args += " -mpegts_m2ts_mode 1"; - } - - if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) - { - args += " -bsf:v h264_mp4toannexb"; - } - - if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps) - { - args += " -copyts -avoid_negative_ts disabled -start_at_zero"; - } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header -fflags +genpts"; - } - - return args; - } - - var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - 5.ToString(UsCulture)); - - args += keyFrameArg; - - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode; - - var hasCopyTs = false; - // Add resolution params, if specified - if (!hasGraphicalSubs) - { - var outputSizeParam = EncodingHelper.GetOutputSizeParam(state, videoCodec); - args += outputSizeParam; - hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; - } - - if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps) - { - if (!hasCopyTs) - { - args += " -copyts"; - } - args += " -avoid_negative_ts disabled -start_at_zero"; - } - - var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions(); - var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, encodingOptions, GetDefaultH264Preset()); - - if (!string.IsNullOrEmpty(qualityParam)) - { - args += " " + qualityParam.Trim(); - } - - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec); - } - - if (!state.RunTimeTicks.HasValue) - { - args += " -flags -global_header"; - } - - return args; - } - - /// - /// Gets audio arguments to pass to ffmpeg - /// - /// The state. - /// System.String. - private string GetAudioArguments(StreamState state) - { - // If the video doesn't have an audio stream, return a default. - if (state.AudioStream == null && state.VideoStream != null) - { - return string.Empty; - } - - // Get the output codec name - var codec = EncodingHelper.GetAudioEncoder(state); - - var args = "-codec:a:0 " + codec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) - { - return args; - } - - // Add the number of audio channels - var channels = state.OutputAudioChannels; - - if (channels.HasValue) - { - args += " -ac " + channels.Value; - } - - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) - { - args += " -ab " + bitrate.Value.ToString(UsCulture); - } - - args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), false); - - return args; + return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset()); } } } \ No newline at end of file diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index f223c99ef..551dbf378 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -32,8 +32,6 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string AudioCodec { get; set; } - public string SubtitleCodec { get; set; } - [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceProfileId { get; set; } @@ -41,6 +39,10 @@ namespace MediaBrowser.Api.Playback public string PlaySessionId { get; set; } public string LiveStreamId { get; set; } public string Tag { get; set; } + public string SegmentContainer { get; set; } + + public int? SegmentLength { get; set; } + public int? MinSegments { get; set; } } public class VideoStreamRequest : StreamRequest diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 912d60889..2f5f6227a 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -60,6 +60,11 @@ namespace MediaBrowser.Api.Playback { get { + if (Request.SegmentLength.HasValue) + { + return Request.SegmentLength.Value; + } + if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { var userAgent = UserAgent ?? string.Empty; @@ -86,6 +91,19 @@ namespace MediaBrowser.Api.Playback } } + public int MinSegments + { + get + { + if (Request.MinSegments.HasValue) + { + return Request.MinSegments.Value; + } + + return SegmentLength >= 10 ? 2 : 3; + } + } + public bool IsSegmentedLiveStream { get @@ -102,7 +120,6 @@ namespace MediaBrowser.Api.Playback } } - public List SupportedSubtitleCodecs { get; set; } public string UserAgent { get; set; } public TranscodingJobType TranscodingType { get; set; } @@ -111,14 +128,12 @@ namespace MediaBrowser.Api.Playback { _mediaSourceManager = mediaSourceManager; _logger = logger; - SupportedSubtitleCodecs = new List(); TranscodingType = transcodingType; } public string MimeType { get; set; } public bool EstimateContentLength { get; set; } - public bool EnableMpegtsM2TsMode { get; set; } public TranscodeSeekInfo TranscodeSeekInfo { get; set; } public long? EncodingDurationTicks { get; set; } @@ -139,6 +154,8 @@ namespace MediaBrowser.Api.Playback DisposeLiveStream(); DisposeLogStream(); DisposeIsoMount(); + + TranscodingJob = null; } private void DisposeLogStream() @@ -191,7 +208,6 @@ namespace MediaBrowser.Api.Playback } public string OutputFilePath { get; set; } - public int? OutputAudioBitrate; public string ActualOutputVideoCodec { @@ -462,5 +478,11 @@ namespace MediaBrowser.Api.Playback return true; } } + + public TranscodingJob TranscodingJob; + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) + { + ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + } } } diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index e010f122c..b0f52dd85 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -1,12 +1,9 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; +using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Connect; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.LiveTv; using System; using System.Linq; using System.Threading.Tasks; @@ -52,16 +49,14 @@ namespace MediaBrowser.Api private readonly IServerApplicationHost _appHost; private readonly IUserManager _userManager; private readonly IConnectManager _connectManager; - private readonly ILiveTvManager _liveTvManager; private readonly IMediaEncoder _mediaEncoder; - public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager, IMediaEncoder mediaEncoder) + public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, IMediaEncoder mediaEncoder) { _config = config; _appHost = appHost; _userManager = userManager; _connectManager = connectManager; - _liveTvManager = liveTvManager; _mediaEncoder = mediaEncoder; } @@ -92,20 +87,6 @@ namespace MediaBrowser.Api PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - var tvConfig = GetLiveTVConfiguration(); - - if (tvConfig.TunerHosts.Count > 0) - { - result.LiveTvTunerPath = tvConfig.TunerHosts[0].Url; - result.LiveTvTunerType = tvConfig.TunerHosts[0].Type; - } - - if (tvConfig.ListingProviders.Count > 0) - { - result.LiveTvGuideProviderId = tvConfig.ListingProviders[0].Id; - result.LiveTvGuideProviderType = tvConfig.ListingProviders[0].Type; - } - return result; } @@ -120,6 +101,7 @@ namespace MediaBrowser.Api config.EnableSeriesPresentationUniqueKey = true; config.EnableLocalizedGuids = true; config.EnableSimpleArtistDetection = true; + config.EnableNormalizedItemByNameIds = true; } public void Post(UpdateStartupConfiguration request) @@ -128,9 +110,6 @@ namespace MediaBrowser.Api _config.Configuration.MetadataCountryCode = request.MetadataCountryCode; _config.Configuration.PreferredMetadataLanguage = request.PreferredMetadataLanguage; _config.SaveConfiguration(); - - var task = UpdateTuners(request); - Task.WaitAll(task); } public object Get(GetStartupUser request) @@ -165,51 +144,6 @@ namespace MediaBrowser.Api return result; } - - private async Task UpdateTuners(UpdateStartupConfiguration request) - { - var config = GetLiveTVConfiguration(); - var save = false; - - if (string.IsNullOrWhiteSpace(request.LiveTvTunerPath) || - string.IsNullOrWhiteSpace(request.LiveTvTunerType)) - { - if (config.TunerHosts.Count > 0) - { - config.TunerHosts.Clear(); - save = true; - } - } - else - { - if (!config.TunerHosts.Any(i => string.Equals(i.Type, request.LiveTvTunerType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.Url, request.LiveTvTunerPath, StringComparison.OrdinalIgnoreCase))) - { - // Add tuner - await _liveTvManager.SaveTunerHost(new TunerHostInfo - { - IsEnabled = true, - Type = request.LiveTvTunerType, - Url = request.LiveTvTunerPath - - }).ConfigureAwait(false); - } - } - - if (save) - { - SaveLiveTVConfiguration(config); - } - } - - private void SaveLiveTVConfiguration(LiveTvOptions config) - { - _config.SaveConfiguration("livetv", config); - } - - private LiveTvOptions GetLiveTVConfiguration() - { - return _config.GetConfiguration("livetv"); - } } public class StartupConfiguration @@ -217,10 +151,6 @@ namespace MediaBrowser.Api public string UICulture { get; set; } public string MetadataCountryCode { get; set; } public string PreferredMetadataLanguage { get; set; } - public string LiveTvTunerType { get; set; } - public string LiveTvTunerPath { get; set; } - public string LiveTvGuideProviderId { get; set; } - public string LiveTvGuideProviderType { get; set; } } public class StartupInfo diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 71c5e732a..43fe74d5f 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -72,6 +72,12 @@ namespace MediaBrowser.Api [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] public bool? EnableUserData { get; set; } + public bool EnableTotalRecordCount { get; set; } + + public GetNextUpEpisodes() + { + EnableTotalRecordCount = true; + } } [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")] @@ -376,7 +382,8 @@ namespace MediaBrowser.Api ParentId = request.ParentId, SeriesId = request.SeriesId, StartIndex = request.StartIndex, - UserId = request.UserId + UserId = request.UserId, + EnableTotalRecordCount = request.EnableTotalRecordCount }); var user = _userManager.GetUserById(request.UserId); diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 20b2529c0..8d83f8a35 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -289,7 +289,12 @@ namespace MediaBrowser.Controller.Entities.Audio } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 74679b474..e26e0dfce 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -118,7 +118,12 @@ namespace MediaBrowser.Controller.Entities.Audio return LibraryManager.GetItemList(query); } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/GameGenre.cs b/MediaBrowser.Controller/Entities/GameGenre.cs index 22a8675c5..4187167b9 100644 --- a/MediaBrowser.Controller/Entities/GameGenre.cs +++ b/MediaBrowser.Controller/Entities/GameGenre.cs @@ -96,7 +96,12 @@ namespace MediaBrowser.Controller.Entities } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 1b746ae51..9769efdd0 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -108,7 +108,12 @@ namespace MediaBrowser.Controller.Entities } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/IHasImages.cs b/MediaBrowser.Controller/Entities/IHasImages.cs index 4c033dc00..2104bee09 100644 --- a/MediaBrowser.Controller/Entities/IHasImages.cs +++ b/MediaBrowser.Controller/Entities/IHasImages.cs @@ -206,6 +206,8 @@ namespace MediaBrowser.Controller.Entities void SetImage(ItemImageInfo image, int index); double? GetDefaultPrimaryImageAspectRatio(); + + int? ProductionYear { get; set; } } public static class HasImagesExtensions diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index ee1aea938..b68681d36 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -133,7 +133,12 @@ namespace MediaBrowser.Controller.Entities } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validFilename = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index b8ad691a9..2e5e6ce43 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -114,7 +114,12 @@ namespace MediaBrowser.Controller.Entities } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 78f907d61..890626419 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -614,7 +614,8 @@ namespace MediaBrowser.Controller.Entities Timestamp = i.Timestamp, Type = type, PlayableStreamFileNames = i.PlayableStreamFileNames.ToList(), - SupportsDirectStream = i.VideoType == VideoType.VideoFile + SupportsDirectStream = i.VideoType == VideoType.VideoFile, + IsRemote = i.IsShortcut }; if (info.Protocol == MediaProtocol.File) diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 75fb69435..b352950a0 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -122,7 +122,12 @@ namespace MediaBrowser.Controller.Entities } } - public static string GetPath(string name, bool normalizeName = true) + public static string GetPath(string name) + { + return GetPath(name, true); + } + + public static string GetPath(string name, bool normalizeName) { // Trim the period at the end because windows will have a hard time with that var validName = normalizeName ? diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index a908d2d3f..5242c5b1f 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -376,19 +376,14 @@ namespace MediaBrowser.Controller.LiveTv /// Task. Task OnRecordingFileDeleted(BaseItem recording); - /// - /// Gets the sat ini mappings. - /// - /// List<NameValuePair>. - List GetSatIniMappings(); - - Task> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken); - Task> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); Task> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); List ListingProviders { get; } + List GetTunerHostTypes(); + Task> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); + event EventHandler> SeriesTimerCancelled; event EventHandler> TimerCancelled; event EventHandler> TimerCreated; diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 5615649c2..fc344298b 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -44,6 +44,8 @@ namespace MediaBrowser.Controller.LiveTv /// The cancellation token. /// Task<List<MediaSourceInfo>>. Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken); + + Task> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken); } public interface IConfigurableTunerHost { diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 4ebee531f..88153868f 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -190,6 +190,7 @@ + diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 80b9cc154..b7b31509c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -154,6 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return "mpegts"; } + // For these need to find out the ffmpeg names if (string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) { @@ -163,6 +164,10 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase)) + { + return null; + } if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase)) { return null; @@ -179,12 +184,23 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + // Seeing reported failures here, not sure yet if this is related to specfying input format if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase)) { return null; } + // obviously don't do this for strm files + if (string.Equals(container, "strm", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + return container; } @@ -661,9 +677,8 @@ namespace MediaBrowser.Controller.MediaEncoding var level = NormalizeTranscodingLevel(state.OutputVideoCodec, request.Level); // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format - // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307 + // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307 if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) { switch (level) @@ -700,10 +715,15 @@ namespace MediaBrowser.Controller.MediaEncoding break; } } + // nvenc doesn't decode with param -level set ?! + if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)){ + param += ""; + } else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)) { param += " -level " + level; } + } if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) @@ -727,12 +747,18 @@ namespace MediaBrowser.Controller.MediaEncoding if (videoStream.IsInterlaced) { - return false; + if (request.DeInterlace) + { + return false; + } } if (videoStream.IsAnamorphic ?? false) { - return false; + if (request.RequireNonAnamorphic) + { + return false; + } } // Can't stream copy if we're burning in subtitles @@ -1561,6 +1587,15 @@ namespace MediaBrowser.Controller.MediaEncoding MediaSourceInfo mediaSource, string requestedUrl) { + if (state == null) + { + throw new ArgumentNullException("state"); + } + if (mediaSource == null) + { + throw new ArgumentNullException("mediaSource"); + } + state.MediaPath = mediaSource.Path; state.InputProtocol = mediaSource.Protocol; state.InputContainer = mediaSource.Container; @@ -1670,9 +1705,21 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_qsv")) { + // qsv decoder does not support 10-bit input + if ((state.VideoStream.BitDepth ?? 8) > 8) + { + return null; + } return "-c:v h264_qsv "; } break; + //case "hevc": + //case "h265": + // if (_mediaEncoder.SupportsDecoder("hevc_qsv")) + // { + // return "-c:v hevc_qsv "; + // } + // break; case "mpeg2video": if (_mediaEncoder.SupportsDecoder("mpeg2_qsv")) { @@ -1715,5 +1762,187 @@ namespace MediaBrowser.Controller.MediaEncoding return threads; } + + public string GetSubtitleEmbedArguments(EncodingJobInfo state) + { + if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) + { + return string.Empty; + } + + var format = state.SupportedSubtitleCodecs.FirstOrDefault(); + string codec; + + if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + codec = "copy"; + } + else + { + codec = format; + } + + // Muxing in dvbsub via either copy or -codec dvbsub does not seem to work + // It doesn't throw any errors but vlc on android will not render them + // They will need to be converted to an alternative format + // TODO: This is incorrectly assuming that dvdsub will be supported by the player + // The api will need to be expanded to accomodate this. + if (string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)) + { + codec = "dvdsub"; + } + + var args = " -codec:s:0 " + codec; + + args += " -disposition:s:0 default"; + + return args; + } + + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset) + { + // Get the output codec name + var videoCodec = GetVideoEncoder(state, encodingOptions); + + var format = string.Empty; + var keyFrame = string.Empty; + + if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) && + state.BaseRequest.Context == EncodingContext.Streaming) + { + // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js + format = " -f mp4 -movflags frag_keyframe+empty_moov"; + } + + var threads = GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)); + + var inputModifier = GetInputModifier(state, encodingOptions); + + return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", + inputModifier, + GetInputArgument(state, encodingOptions), + keyFrame, + GetMapArgs(state), + GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset), + threads, + GetProgressiveVideoAudioArguments(state, encodingOptions), + GetSubtitleEmbedArguments(state), + format, + outputPath + ).Trim(); + } + + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset) + { + var args = "-codec:v:0 " + videoCodec; + + if (state.EnableMpegtsM2TsMode) + { + args += " -mpegts_m2ts_mode 1"; + } + + if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + { + args += " -bsf:v h264_mp4toannexb"; + } + + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) + { + args += " -copyts -avoid_negative_ts disabled -start_at_zero"; + } + + if (!state.RunTimeTicks.HasValue) + { + args += " -flags -global_header -fflags +genpts"; + } + + return args; + } + + var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", + 5.ToString(_usCulture)); + + args += keyFrameArg; + + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.BaseRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode; + + var hasCopyTs = false; + // Add resolution params, if specified + if (!hasGraphicalSubs) + { + var outputSizeParam = GetOutputSizeParam(state, videoCodec); + args += outputSizeParam; + hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; + } + + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) + { + if (!hasCopyTs) + { + args += " -copyts"; + } + args += " -avoid_negative_ts disabled -start_at_zero"; + } + + var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset); + + if (!string.IsNullOrEmpty(qualityParam)) + { + args += " " + qualityParam.Trim(); + } + + // This is for internal graphical subs + if (hasGraphicalSubs) + { + args += GetGraphicalSubtitleParam(state, videoCodec); + } + + if (!state.RunTimeTicks.HasValue) + { + args += " -flags -global_header"; + } + + return args; + } + + public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions) + { + // If the video doesn't have an audio stream, return a default. + if (state.AudioStream == null && state.VideoStream != null) + { + return string.Empty; + } + + // Get the output codec name + var codec = GetAudioEncoder(state); + + var args = "-codec:a:0 " + codec; + + if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return args; + } + + // Add the number of audio channels + var channels = state.OutputAudioChannels; + + if (channels.HasValue) + { + args += " -ac " + channels.Value; + } + + var bitrate = state.OutputAudioBitrate; + + if (bitrate.HasValue) + { + args += " -ab " + bitrate.Value.ToString(_usCulture); + } + + args += " " + GetAudioFilterParam(state, encodingOptions, false); + + return args; + } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index a18b86432..f5878864b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -12,7 +12,7 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Controller.MediaEncoding { // For now, a common base class until the API and MediaEncoding classes are unified - public class EncodingJobInfo + public abstract class EncodingJobInfo { private readonly ILogger _logger; @@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding public int? OutputVideoBitrate { get; set; } public MediaStream SubtitleStream { get; set; } public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; } + public List SupportedSubtitleCodecs { get; set; } public int InternalSubtitleStreamOffset { get; set; } public MediaSourceInfo MediaSource { get; set; } @@ -52,6 +53,8 @@ namespace MediaBrowser.Controller.MediaEncoding public string InputContainer { get; set; } public IsoType? IsoType { get; set; } + public bool EnableMpegtsM2TsMode { get; set; } + public BaseEncodingJobOptions BaseRequest { get; set; } public long? StartTimeTicks @@ -64,6 +67,7 @@ namespace MediaBrowser.Controller.MediaEncoding get { return BaseRequest.CopyTimestamps; } } + public int? OutputAudioBitrate; public int? OutputAudioChannels; public int? OutputAudioSampleRate; public bool DeInterlace { get; set; } @@ -74,8 +78,9 @@ namespace MediaBrowser.Controller.MediaEncoding _logger = logger; RemoteHttpHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); PlayableStreamFileNames = new List(); + SupportedAudioCodecs = new List(); SupportedVideoCodecs = new List(); - SupportedVideoCodecs = new List(); + SupportedSubtitleCodecs = new List(); } /// @@ -110,5 +115,7 @@ namespace MediaBrowser.Controller.MediaEncoding IsoMount = null; } } + + public abstract void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 4bb180d4b..73be78dc9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -14,7 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding public string AudioCodec { get; set; } public DeviceProfile DeviceProfile { get; set; } - public EncodingContext Context { get; set; } public bool ReadInputAtNativeFramerate { get; set; } @@ -46,7 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding AudioBitRate = info.AudioBitrate; AudioSampleRate = info.TargetAudioSampleRate; DeviceProfile = deviceProfile; - VideoCodec = info.VideoCodec; + VideoCodec = info.TargetVideoCodec; VideoBitRate = info.VideoBitrate; AudioStreamIndex = info.AudioStreamIndex; MaxRefFrames = info.MaxRefFrames; @@ -185,6 +184,8 @@ namespace MediaBrowser.Controller.MediaEncoding [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxVideoBitDepth { get; set; } public bool RequireAvc { get; set; } + public bool DeInterlace { get; set; } + public bool RequireNonAnamorphic { get; set; } public int? TranscodingMaxAudioChannels { get; set; } public int? CpuCoreLimit { get; set; } public string OutputContainer { get; set; } @@ -196,6 +197,8 @@ namespace MediaBrowser.Controller.MediaEncoding [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string VideoCodec { get; set; } + public string SubtitleCodec { get; set; } + /// /// Gets or sets the index of the audio stream. /// @@ -210,9 +213,12 @@ namespace MediaBrowser.Controller.MediaEncoding [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? VideoStreamIndex { get; set; } + public EncodingContext Context { get; set; } + public BaseEncodingJobOptions() { EnableAutoStreamCopy = true; + Context = EncodingContext.Streaming; } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs similarity index 64% rename from MediaBrowser.MediaEncoding/Encoder/JobLogger.cs rename to MediaBrowser.Controller/MediaEncoding/JobLogger.cs index cb6e58f17..03e4f7771 100644 --- a/MediaBrowser.MediaEncoding/Encoder/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using System.Text; -namespace MediaBrowser.MediaEncoding.Encoder +namespace MediaBrowser.Controller.MediaEncoding { public class JobLogger { @@ -18,7 +18,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; } - public async void StartStreamingLog(EncodingJob transcodingJob, Stream source, Stream target) + public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target) { try { @@ -28,35 +28,41 @@ namespace MediaBrowser.MediaEncoding.Encoder { var line = await reader.ReadLineAsync().ConfigureAwait(false); - ParseLogLine(line, transcodingJob); + ParseLogLine(line, state); var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.FlushAsync().ConfigureAwait(false); } } } + catch (ObjectDisposedException) + { + // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux + } catch (Exception ex) { _logger.ErrorException("Error reading ffmpeg log", ex); } } - private void ParseLogLine(string line, EncodingJob transcodingJob) + private void ParseLogLine(string line, EncodingJobInfo state) { float? framerate = null; double? percent = null; TimeSpan? transcodingPosition = null; long? bytesTranscoded = null; + int? bitRate = null; var parts = line.Split(' '); - var totalMs = transcodingJob.RunTimeTicks.HasValue - ? TimeSpan.FromTicks(transcodingJob.RunTimeTicks.Value).TotalMilliseconds + var totalMs = state.RunTimeTicks.HasValue + ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds : 0; - var startMs = transcodingJob.Options.StartTimeTicks.HasValue - ? TimeSpan.FromTicks(transcodingJob.Options.StartTimeTicks.Value).TotalMilliseconds + var startMs = state.BaseRequest.StartTimeTicks.HasValue + ? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds : 0; for (var i = 0; i < parts.Length; i++) @@ -74,7 +80,7 @@ namespace MediaBrowser.MediaEncoding.Encoder framerate = val; } } - else if (transcodingJob.RunTimeTicks.HasValue && + else if (state.RunTimeTicks.HasValue && part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) { var time = part.Split(new[] { '=' }, 2).Last(); @@ -111,11 +117,32 @@ namespace MediaBrowser.MediaEncoding.Encoder } } } + else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + float val; + + if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val)) + { + bitRate = (int)Math.Ceiling(val * scale.Value); + } + } + } } if (framerate.HasValue || percent.HasValue) { - transcodingJob.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded); + state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } } diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs index 272fa8b3b..62c13fbb5 100644 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ b/MediaBrowser.Controller/Net/StaticResultOptions.cs @@ -23,21 +23,18 @@ namespace MediaBrowser.Controller.Net public Action OnComplete { get; set; } public Action OnError { get; set; } + public string Path { get; set; } + + public FileShareMode FileShare { get; set; } + public StaticResultOptions() { ResponseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + FileShare = FileShareMode.Read; } } public class StaticFileResultOptions : StaticResultOptions { - public string Path { get; set; } - - public FileShareMode FileShare { get; set; } - - public StaticFileResultOptions() - { - FileShare = FileShareMode.Read; - } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs index a4db8f3b8..05b3ca5fc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { } - protected override Task GetCommandLineArguments(EncodingJob state) + protected override string GetCommandLineArguments(EncodingJob state) { var audioTranscodeParams = new List(); @@ -78,7 +78,7 @@ namespace MediaBrowser.MediaEncoding.Encoder mapArgs, metadata).Trim(); - return Task.FromResult(result); + return result; } protected override string GetOutputFileExtension(EncodingJob state) diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index b0b37f2d6..b6e695f1b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Encoder IProgress progress, CancellationToken cancellationToken) { - var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager) + var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager, MediaEncoder) .CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false); encodingJob.OutputFilePath = GetOutputFilePath(encodingJob); @@ -76,7 +76,7 @@ namespace MediaBrowser.MediaEncoding.Encoder await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false); - var commandLineArgs = await GetCommandLineArguments(encodingJob).ConfigureAwait(false); + var commandLineArgs = GetCommandLineArguments(encodingJob); var process = ProcessFactory.Create(new ProcessOptions { @@ -242,7 +242,7 @@ namespace MediaBrowser.MediaEncoding.Encoder private void OnTranscodeBeginning(EncodingJob job) { - job.ReportTranscodingProgress(null, null, null, null); + job.ReportTranscodingProgress(null, null, null, null, null); } private void OnTranscodeFailedToStart(string path, EncodingJob job) @@ -265,7 +265,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return ConfigurationManager.GetConfiguration("encoding"); } - protected abstract Task GetCommandLineArguments(EncodingJob job); + protected abstract string GetCommandLineArguments(EncodingJob job); private string GetOutputFilePath(EncodingJob state) { diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 9c1189f6c..15221c2ac 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -89,6 +89,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var found = new List(); var required = new[] { + "mpeg2video", "h264_qsv", "hevc_qsv", "mpeg2_qsv", diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs index f6895696a..883709de8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJob.cs @@ -36,7 +36,6 @@ namespace MediaBrowser.MediaEncoding.Encoder public string MimeType { get; set; } public bool EstimateContentLength { get; set; } - public bool EnableMpegtsM2TsMode { get; set; } public TranscodeSeekInfo TranscodeSeekInfo { get; set; } public long? EncodingDurationTicks { get; set; } public string LiveStreamId { get; set; } @@ -109,7 +108,6 @@ namespace MediaBrowser.MediaEncoding.Encoder } public string OutputFilePath { get; set; } - public int? OutputAudioBitrate; public string ActualOutputVideoCodec { @@ -379,7 +377,7 @@ namespace MediaBrowser.MediaEncoding.Encoder return count; } - public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded) + public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; @@ -387,8 +385,8 @@ namespace MediaBrowser.MediaEncoding.Encoder if (!percentComplete.HasValue && ticks.HasValue && RunTimeTicks.HasValue) { - var pct = ticks.Value/RunTimeTicks.Value; - percentComplete = pct*100; + var pct = ticks.Value / RunTimeTicks.Value; + percentComplete = pct * 100; } if (percentComplete.HasValue) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index 4b336e671..3e99d68ce 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -22,15 +22,17 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _config; + private readonly IMediaEncoder _mediaEncoder; protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config) + public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder) { _logger = logger; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; _config = config; + _mediaEncoder = mediaEncoder; } public async Task CreateJob(EncodingJobOptions options, EncodingHelper encodingHelper, bool isVideoRequest, IProgress progress, CancellationToken cancellationToken) @@ -61,6 +63,13 @@ namespace MediaBrowser.MediaEncoding.Encoder request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(); } + if (!string.IsNullOrWhiteSpace(request.SubtitleCodec)) + { + state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToSubtitleCodec(i)) + ?? state.SupportedSubtitleCodecs.FirstOrDefault(); + } + var item = _libraryManager.GetItemById(request.ItemId); state.ItemType = item.GetType().Name; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 580f5c615..97f2c57eb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -647,9 +647,9 @@ namespace MediaBrowser.MediaEncoding.Encoder var videoStream = mediaInfo.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - if (videoStream != null) + if (videoStream != null && !videoStream.IsInterlaced) { - var isInterlaced = await DetectInterlaced(mediaInfo, videoStream, inputPath, probeSizeArgument).ConfigureAwait(false); + var isInterlaced = DetectInterlaced(mediaInfo, videoStream); if (isInterlaced) { @@ -672,7 +672,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - private async Task DetectInterlaced(MediaSourceInfo video, MediaStream videoStream, string inputPath, string probeSizeArgument) + private bool DetectInterlaced(MediaSourceInfo video, MediaStream videoStream) { var formats = (video.Container ?? string.Empty).Split(',').ToList(); var enableInterlacedDection = formats.Contains("vob", StringComparer.OrdinalIgnoreCase) || @@ -698,165 +698,9 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - if (video.Protocol != MediaProtocol.File) - { - return false; - } - - var args = "{0} -i {1} -map 0:v:{2} -an -filter:v idet -frames:v 500 -an -f null /dev/null"; - - var process = _processFactory.Create(new ProcessOptions - { - CreateNoWindow = true, - UseShellExecute = false, - - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - RedirectStandardError = true, - FileName = FFMpegPath, - Arguments = string.Format(args, probeSizeArgument, inputPath, videoStream.Index.ToString(CultureInfo.InvariantCulture)).Trim(), - - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - }); - - _logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - var idetFoundInterlaced = false; - - using (var processWrapper = new ProcessWrapper(process, this, _logger)) - { - try - { - StartProcess(processWrapper); - } - catch (Exception ex) - { - _logger.ErrorException("Error starting ffprobe", ex); - - throw; - } - - try - { - //process.BeginOutputReadLine(); - - using (var reader = new StreamReader(process.StandardError.BaseStream)) - { - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - - if (line.StartsWith("[Parsed_idet", StringComparison.OrdinalIgnoreCase)) - { - var idetResult = AnalyzeIdetResult(line); - - if (idetResult.HasValue) - { - if (!idetResult.Value) - { - return false; - } - - idetFoundInterlaced = true; - } - } - } - } - - } - catch - { - StopProcess(processWrapper, 100); - - throw; - } - } - - return idetFoundInterlaced; - } - - private bool? AnalyzeIdetResult(string line) - { - // As you can see, the filter only guessed one frame as progressive. - // Results like this are pretty typical. So if less than 30% of the detections are in the "Undetermined" category, then I only consider the video to be interlaced if at least 65% of the identified frames are in either the TFF or BFF category. - // In this case (310 + 311)/(622) = 99.8% which is well over the 65% metric. I may refine that number with more testing but I honestly do not believe I will need to. - // http://awel.domblogger.net/videoTranscode/interlace.html - var index = line.IndexOf("detection:", StringComparison.OrdinalIgnoreCase); - - if (index == -1) - { - return null; - } - - line = line.Substring(index).Trim(); - var parts = line.Split(' ').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => i.Trim()).ToList(); - - if (parts.Count < 2) - { - return null; - } - double tff = 0; - double bff = 0; - double progressive = 0; - double undetermined = 0; - double total = 0; - - for (var i = 0; i < parts.Count - 1; i++) - { - var part = parts[i]; - - if (string.Equals(part, "tff:", StringComparison.OrdinalIgnoreCase)) - { - tff = GetNextPart(parts, i); - total += tff; - } - else if (string.Equals(part, "bff:", StringComparison.OrdinalIgnoreCase)) - { - bff = GetNextPart(parts, i); - total += tff; - } - else if (string.Equals(part, "progressive:", StringComparison.OrdinalIgnoreCase)) - { - progressive = GetNextPart(parts, i); - total += progressive; - } - else if (string.Equals(part, "undetermined:", StringComparison.OrdinalIgnoreCase)) - { - undetermined = GetNextPart(parts, i); - total += undetermined; - } - } - - if (total == 0) - { - return null; - } - - if ((undetermined / total) >= .3) - { - return false; - } - - if (((tff + bff) / total) >= .4) - { - return true; - } - return false; } - private int GetNextPart(List parts, int index) - { - var next = parts[index + 1]; - - int value; - if (int.TryParse(next, NumberStyles.Any, CultureInfo.InvariantCulture, out value)) - { - return value; - } - return 0; - } - /// /// The us culture /// diff --git a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs index 52ef4d834..96c126923 100644 --- a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs @@ -18,143 +18,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { } - protected override async Task GetCommandLineArguments(EncodingJob state) + protected override string GetCommandLineArguments(EncodingJob state) { // Get the output codec name var encodingOptions = GetEncodingOptions(); - var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions); - var format = string.Empty; - var keyFrame = string.Empty; - - if (string.Equals(Path.GetExtension(state.OutputFilePath), ".mp4", StringComparison.OrdinalIgnoreCase) && - state.Options.Context == EncodingContext.Streaming) - { - // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js - format = " -f mp4 -movflags frag_keyframe+empty_moov"; - } - - var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)); - - var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); - - var videoArguments = await GetVideoArguments(state, videoCodec).ConfigureAwait(false); - - return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"", - inputModifier, - EncodingHelper.GetInputArgument(state, encodingOptions), - keyFrame, - EncodingHelper.GetMapArgs(state), - videoArguments, - threads, - GetAudioArguments(state), - format, - state.OutputFilePath - ).Trim(); - } - - /// - /// Gets video arguments to pass to ffmpeg - /// - /// The state. - /// The video codec. - /// System.String. - private async Task GetVideoArguments(EncodingJob state, string videoCodec) - { - var args = "-codec:v:0 " + videoCodec; - - if (state.EnableMpegtsM2TsMode) - { - args += " -mpegts_m2ts_mode 1"; - } - - var isOutputMkv = string.Equals(state.Options.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase); - - if (state.RunTimeTicks.HasValue) - { - //args += " -copyts -avoid_negative_ts disabled -start_at_zero"; - } - - if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) - { - args += " -bsf:v h264_mp4toannexb"; - } - - return args; - } - - var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - 5.ToString(UsCulture)); - - args += keyFrameArg; - - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode; - - // Add resolution params, if specified - if (!hasGraphicalSubs) - { - args += EncodingHelper.GetOutputSizeParam(state, videoCodec); - } - - var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, GetEncodingOptions(), "superfast"); - - if (!string.IsNullOrEmpty(qualityParam)) - { - args += " " + qualityParam.Trim(); - } - - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec); - } - - return args; - } - - /// - /// Gets audio arguments to pass to ffmpeg - /// - /// The state. - /// System.String. - private string GetAudioArguments(EncodingJob state) - { - // If the video doesn't have an audio stream, return a default. - if (state.AudioStream == null && state.VideoStream != null) - { - return string.Empty; - } - - // Get the output codec name - var codec = EncodingHelper.GetAudioEncoder(state); - - var args = "-codec:a:0 " + codec; - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) - { - return args; - } - - // Add the number of audio channels - var channels = state.OutputAudioChannels; - - if (channels.HasValue) - { - args += " -ac " + channels.Value; - } - - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) - { - args += " -ab " + bitrate.Value.ToString(UsCulture); - } - - args += " " + EncodingHelper.GetAudioFilterParam(state, GetEncodingOptions(), false); - - return args; + return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, state.OutputFilePath, "superfast"); } protected override string GetOutputFileExtension(EncodingJob state) diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 63e789a59..ee19c4b66 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -55,7 +55,6 @@ - diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index f32dd178f..eef273250 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -264,6 +264,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// The loro_surmixlev. public string loro_surmixlev { get; set; } + public string field_order { get; set; } + /// /// Gets or sets the disposition. /// diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 7927ddb6a..8b20dca1b 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -508,6 +508,11 @@ namespace MediaBrowser.MediaEncoding.Probing stream.IsAVC = false; } + if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase)) + { + stream.IsInterlaced = true; + } + // Filter out junk if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 4a1bf5305..77b976206 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -734,16 +734,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - var charsetFromLanguage = string.IsNullOrWhiteSpace(language) - ? null - : GetSubtitleFileCharacterSetFromLanguage(language); - - // This assumption should only be made for external subtitles - if (!string.IsNullOrWhiteSpace(charsetFromLanguage) && !string.Equals(charsetFromLanguage, "windows-1252", StringComparison.OrdinalIgnoreCase)) - { - return charsetFromLanguage; - } - var charset = await DetectCharset(path, language, protocol, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(charset)) @@ -756,7 +746,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles return charset; } - return charsetFromLanguage; + if (!string.IsNullOrWhiteSpace(language)) + { + return GetSubtitleFileCharacterSetFromLanguage(language); + } + + return null; } public string GetSubtitleFileCharacterSetFromLanguage(string language) @@ -854,4 +849,4 @@ namespace MediaBrowser.MediaEncoding.Subtitles throw new ArgumentOutOfRangeException("protocol"); } } -} +} \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 7af0acc59..9cd656fa7 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -16,6 +16,8 @@ public bool EnableAutomaticSeriesGrouping { get; set; } public bool EnableEmbeddedTitles { get; set; } + public int AutomaticRefreshIntervalDays { get; set; } + /// /// Gets or sets the preferred metadata language. /// diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c2b1e3c89..0562d0ac5 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -48,6 +48,7 @@ namespace MediaBrowser.Model.Configuration public bool EnableHttps { get; set; } public bool EnableSeriesPresentationUniqueKey { get; set; } public bool EnableLocalizedGuids { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } /// /// Gets or sets the value pointing to the file system where the ssl certiifcate is located.. diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs index 7e2002f17..dbd574f86 100644 --- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs +++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs @@ -21,6 +21,7 @@ NumVideoStreams = 17, IsSecondaryAudio = 18, VideoCodecTag = 19, - IsAvc = 20 + IsAvc = 20, + IsInterlaced = 21 } } \ No newline at end of file diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 480eb23a5..36df67b34 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -231,6 +231,7 @@ namespace MediaBrowser.Model.Dlna { playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); } + playlistItem.SubProtocol = transcodingProfile.Protocol; List audioCodecProfiles = new List(); @@ -323,7 +324,7 @@ namespace MediaBrowser.Model.Dlna if (directPlayProfile != null) { // While options takes the network and other factors into account. Only applies to direct stream - if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true)) && options.EnableDirectStream) + if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true), PlayMethod.DirectStream) && options.EnableDirectStream) { playMethods.Add(PlayMethod.DirectStream); } @@ -331,7 +332,7 @@ namespace MediaBrowser.Model.Dlna // The profile describes what the device supports // If device requirements are satisfied then allow both direct stream and direct play if (item.SupportsDirectPlay && - IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true)) && options.EnableDirectPlay) + IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true), PlayMethod.DirectPlay) && options.EnableDirectPlay) { playMethods.Add(PlayMethod.DirectPlay); } @@ -479,10 +480,19 @@ namespace MediaBrowser.Model.Dlna playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(','); - playlistItem.VideoCodec = transcodingProfile.VideoCodec; + playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(','); playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps; playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; + if (transcodingProfile.MinSegments > 0) + { + playlistItem.MinSegments = transcodingProfile.MinSegments; + } + if (transcodingProfile.SegmentLength > 0) + { + playlistItem.SegmentLength = transcodingProfile.SegmentLength; + } + if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels)) { int transcodingMaxAudioChannels; @@ -895,7 +905,7 @@ namespace MediaBrowser.Model.Dlna } } - return IsAudioEligibleForDirectPlay(item, maxBitrate); + return IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod); } public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, string transcodingSubProtocol, string transcodingContainer) @@ -1025,23 +1035,29 @@ namespace MediaBrowser.Model.Dlna return null; } - private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate) + private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate, PlayMethod playMethod) { + // Don't restrict by bitrate if coming from an external domain + if (item.IsRemote) + { + return true; + } + if (!maxBitrate.HasValue) { - _logger.Info("Cannot direct play due to unknown supported bitrate"); + _logger.Info("Cannot "+ playMethod + " due to unknown supported bitrate"); return false; } if (!item.Bitrate.HasValue) { - _logger.Info("Cannot direct play due to unknown content bitrate"); + _logger.Info("Cannot " + playMethod + " due to unknown content bitrate"); return false; } if (item.Bitrate.Value > maxBitrate.Value) { - _logger.Info("Bitrate exceeds DirectPlay limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture)); + _logger.Info("Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture)); return false; } @@ -1137,6 +1153,37 @@ namespace MediaBrowser.Model.Dlna break; } case ProfileConditionValue.IsAnamorphic: + { + bool isAnamorphic; + if (bool.TryParse(value, out isAnamorphic)) + { + if (isAnamorphic && condition.Condition == ProfileConditionType.Equals) + { + item.RequireNonAnamorphic = true; + } + else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals) + { + item.RequireNonAnamorphic = true; + } + } + break; + } + case ProfileConditionValue.IsInterlaced: + { + bool isInterlaced; + if (bool.TryParse(value, out isInterlaced)) + { + if (isInterlaced && condition.Condition == ProfileConditionType.Equals) + { + item.DeInterlace = true; + } + else if (!isInterlaced && condition.Condition == ProfileConditionType.NotEquals) + { + item.DeInterlace = true; + } + } + break; + } case ProfileConditionValue.AudioProfile: case ProfileConditionValue.Has64BitOffsets: case ProfileConditionValue.PacketLength: diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index a85e6085b..5705e6477 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -6,6 +6,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace MediaBrowser.Model.Dlna @@ -18,6 +19,7 @@ namespace MediaBrowser.Model.Dlna public StreamInfo() { AudioCodecs = new string[] { }; + VideoCodecs = new string[] { }; SubtitleCodecs = new string[] { }; } @@ -34,13 +36,18 @@ namespace MediaBrowser.Model.Dlna public long StartPositionTicks { get; set; } - public string VideoCodec { get; set; } public string VideoProfile { get; set; } + public int? SegmentLength { get; set; } + public int? MinSegments { get; set; } + public bool RequireAvc { get; set; } + public bool DeInterlace { get; set; } + public bool RequireNonAnamorphic { get; set; } public bool CopyTimestamps { get; set; } public bool EnableSubtitlesInManifest { get; set; } public string[] AudioCodecs { get; set; } + public string[] VideoCodecs { get; set; } public int? AudioStreamIndex { get; set; } @@ -204,11 +211,15 @@ namespace MediaBrowser.Model.Dlna string.Empty : string.Join(",", item.AudioCodecs); + string videoCodecs = item.VideoCodecs.Length == 0 ? + string.Empty : + string.Join(",", item.VideoCodecs); + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); list.Add(new NameValuePair("Static", item.IsDirectStream.ToString().ToLower())); - list.Add(new NameValuePair("VideoCodec", item.VideoCodec ?? string.Empty)); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); list.Add(new NameValuePair("AudioCodec", audioCodecs)); list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioStreamIndex.Value) : string.Empty)); list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? StringHelper.ToStringCultureInvariant(item.SubtitleStreamIndex.Value) : string.Empty)); @@ -232,7 +243,9 @@ namespace MediaBrowser.Model.Dlna // } //} - if (StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls") && !forceStartPosition) + var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls"); + + if (isHls && !forceStartPosition) { list.Add(new NameValuePair("StartTimeTicks", string.Empty)); } @@ -276,6 +289,24 @@ namespace MediaBrowser.Model.Dlna list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString().ToLower())); + list.Add(new NameValuePair("DeInterlace", item.DeInterlace.ToString().ToLower())); + + if (!isDlna && isHls) + { + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + + if (item.SegmentLength.HasValue) + { + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); + } + + if (item.MinSegments.HasValue) + { + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); + } + } + return list; } @@ -609,9 +640,34 @@ namespace MediaBrowser.Model.Dlna } } + public string TargetVideoCodec + { + get + { + MediaStream stream = TargetVideoStream; + + string inputCodec = stream == null ? null : stream.Codec; + + if (IsDirectStream) + { + return inputCodec; + } + + foreach (string codec in VideoCodecs) + { + if (StringHelper.EqualsIgnoreCase(codec, inputCodec)) + { + return codec; + } + } + + return VideoCodecs.Length == 0 ? null : VideoCodecs[0]; + } + } + /// - /// Predicts the audio channels that will be in the output stream - /// + /// Predicts the audio channels that will be in the output stream + /// public long? TargetSize { get diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index 76f2332f2..350556e90 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -42,6 +42,12 @@ namespace MediaBrowser.Model.Dlna [XmlAttribute("maxAudioChannels")] public string MaxAudioChannels { get; set; } + [XmlAttribute("minSegments")] + public int MinSegments { get; set; } + + [XmlAttribute("segmentLength")] + public int SegmentLength { get; set; } + public List GetAudioCodecs() { List list = new List(); diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index f6d1bb351..f90119cf3 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -10,6 +10,8 @@ namespace MediaBrowser.Model.IO /// public interface IFileSystem { + void AddShortcutHandler(IShortcutHandler handler); + /// /// Determines whether the specified filename is shortcut. /// diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 79a484a46..91ea977f7 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Model.LiveTv TunerHosts = new List(); ListingProviders = new List(); MediaLocationsCreated = new string[] { }; - RecordingEncodingFormat = "mp4"; + RecordingEncodingFormat = "mkv"; RecordingPostProcessorArguments = "\"{path}\""; EnableRecordingEncoding = true; } @@ -46,14 +46,13 @@ namespace MediaBrowser.Model.LiveTv public string Url { get; set; } public string Type { get; set; } public string DeviceId { get; set; } + public string FriendlyName { get; set; } public bool ImportFavoritesOnly { get; set; } public bool AllowHWTranscoding { get; set; } - public bool IsEnabled { get; set; } public bool EnableTvgId { get; set; } public TunerHostInfo() { - IsEnabled = true; AllowHWTranscoding = true; } } diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs index 57a2254b0..16c9464ac 100644 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs +++ b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs @@ -27,9 +27,11 @@ namespace MediaBrowser.Model.MediaInfo public bool EnableDirectPlay { get; set; } public bool EnableDirectStream { get; set; } public bool EnableTranscoding { get; set; } + public bool ForceDirectPlayRemoteMediaSource { get; set; } public PlaybackInfoRequest() { + ForceDirectPlayRemoteMediaSource = true; EnableDirectPlay = true; EnableDirectStream = true; EnableTranscoding = true; diff --git a/MediaBrowser.Model/Net/IAcceptSocket.cs b/MediaBrowser.Model/Net/IAcceptSocket.cs index cac23b337..4262e2390 100644 --- a/MediaBrowser.Model/Net/IAcceptSocket.cs +++ b/MediaBrowser.Model/Net/IAcceptSocket.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Model.Net { @@ -13,6 +15,7 @@ namespace MediaBrowser.Model.Net void Bind(IpEndPointInfo endpoint); void Connect(IpEndPointInfo endPoint); void StartAccept(Action onAccept, Func isClosed); + Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken); } public class SocketCreateException : Exception diff --git a/MediaBrowser.Model/Net/ISocket.cs b/MediaBrowser.Model/Net/ISocket.cs index 90070b128..61fc0e28b 100644 --- a/MediaBrowser.Model/Net/ISocket.cs +++ b/MediaBrowser.Model/Net/ISocket.cs @@ -24,5 +24,6 @@ namespace MediaBrowser.Model.Net /// Sends a UDP message to a particular end point (uni or multicast). /// Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken); + Task SendWithLockAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index 4b70f3362..e7dbf6cb1 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -14,6 +14,8 @@ namespace MediaBrowser.Model.Net /// A implementation. ISocket CreateUdpSocket(int localPort); + ISocket CreateUdpBroadcastSocket(int localPort); + ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort); /// diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index b5f50bde0..e3b0d578a 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -55,9 +55,12 @@ namespace MediaBrowser.Model.Querying /// The enable image types. public ImageType[] EnableImageTypes { get; set; } + public bool EnableTotalRecordCount { get; set; } + public NextUpQuery() { EnableImageTypes = new ImageType[] {}; + EnableTotalRecordCount = true; } } } diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs index b10e12813..f61a94f6e 100644 --- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs +++ b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs @@ -8,4 +8,9 @@ namespace MediaBrowser.Model.Services { Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken); } + + public interface IFileWriter + { + Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken); + } } diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index e9a9f1c5b..115ba25ce 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; namespace MediaBrowser.Model.Services { @@ -151,6 +154,7 @@ namespace MediaBrowser.Model.Services //Add Metadata to Response Dictionary Items { get; } - } + Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken); + } } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 75c4b46b2..7c797133f 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -172,18 +172,11 @@ namespace MediaBrowser.Providers.Manager try { - var currentFile = _fileSystem.GetFileInfo(currentPath); - - // This will fail if the file is hidden - if (currentFile.Exists) - { - if (currentFile.IsHidden) - { - _fileSystem.SetHidden(currentFile.FullName, false); - } - - _fileSystem.DeleteFile(currentFile.FullName); - } + _fileSystem.DeleteFile(currentPath); + } + catch (FileNotFoundException) + { + } finally { diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 9dff243c1..31cdc164c 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -373,20 +373,15 @@ namespace MediaBrowser.Providers.Manager continue; } - // Delete the source file - var currentFile = _fileSystem.GetFileInfo(image.Path); - - // Deletion will fail if the file is hidden so remove the attribute first - if (currentFile.Exists) + try { - if (currentFile.IsHidden) - { - _fileSystem.SetHidden(currentFile.FullName, false); - } - - _fileSystem.DeleteFile(currentFile.FullName); + _fileSystem.DeleteFile(image.Path); deleted = true; } + catch (FileNotFoundException) + { + + } } foreach (var image in deletedImages) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index bdfe13c1d..59a2278a1 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -45,12 +45,21 @@ namespace MediaBrowser.Providers.Manager var updateType = ItemUpdateType.None; var requiresRefresh = false; + var libraryOptions = LibraryManager.GetLibraryOptions((BaseItem)item); + if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) { // TODO: If this returns true, should we instead just change metadata refresh mode to Full? requiresRefresh = item.RequiresRefresh(); } + if (!requiresRefresh && + libraryOptions.AutomaticRefreshIntervalDays > 0 && + (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays) + { + requiresRefresh = true; + } + var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem); var localImagesFailed = false; @@ -116,8 +125,6 @@ namespace MediaBrowser.Providers.Manager } } - LibraryOptions libraryOptions = null; - // Next run remote image providers, but only if local image providers didn't throw an exception if (!localImagesFailed && refreshOptions.ImageRefreshMode != ImageRefreshMode.ValidationOnly) { @@ -125,11 +132,6 @@ namespace MediaBrowser.Providers.Manager if (providers.Count > 0) { - if (libraryOptions == null) - { - libraryOptions = LibraryManager.GetLibraryOptions((BaseItem)item); - } - var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, config, cancellationToken).ConfigureAwait(false); updateType = updateType | result.UpdateType; @@ -177,11 +179,6 @@ namespace MediaBrowser.Providers.Manager item.DateLastRefreshed = default(DateTime); } - if (libraryOptions == null) - { - libraryOptions = LibraryManager.GetLibraryOptions((BaseItem)item); - } - // Save to database await SaveItem(metadataResult, libraryOptions, updateType, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index e6eb0951d..4aefb62c8 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -45,14 +45,21 @@ namespace MediaBrowser.Providers.Music public async Task> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { var releaseId = searchInfo.GetReleaseId(); + var releaseGroupId = searchInfo.GetReleaseGroupId(); string url = null; var isNameSearch = false; + bool forceMusicBrainzProper = false; if (!string.IsNullOrEmpty(releaseId)) { url = string.Format("/ws/2/release/?query=reid:{0}", releaseId); } + else if (!string.IsNullOrEmpty(releaseGroupId)) + { + url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); + forceMusicBrainzProper = true; + } else { var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); @@ -75,7 +82,7 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(url)) { - using (var stream = await GetMusicBrainzResponse(url, isNameSearch, cancellationToken).ConfigureAwait(false)) + using (var stream = await GetMusicBrainzResponse(url, isNameSearch, forceMusicBrainzProper, cancellationToken).ConfigureAwait(false)) { return GetResultsFromResponse(stream); } @@ -131,7 +138,14 @@ namespace MediaBrowser.Providers.Music Item = new MusicAlbum() }; - if (string.IsNullOrEmpty(releaseId)) + // If we have a release group Id but not a release Id... + if (string.IsNullOrWhiteSpace(releaseId) && !string.IsNullOrWhiteSpace(releaseGroupId)) + { + releaseId = await GetReleaseIdFromReleaseGroupId(releaseGroupId, cancellationToken).ConfigureAwait(false); + result.HasMetadata = true; + } + + if (string.IsNullOrWhiteSpace(releaseId)) { var artistMusicBrainzId = id.GetMusicBrainzArtistId(); @@ -139,13 +153,13 @@ namespace MediaBrowser.Providers.Music if (releaseResult != null) { - if (!string.IsNullOrEmpty(releaseResult.ReleaseId)) + if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseId)) { releaseId = releaseResult.ReleaseId; result.HasMetadata = true; } - if (!string.IsNullOrEmpty(releaseResult.ReleaseGroupId)) + if (!string.IsNullOrWhiteSpace(releaseResult.ReleaseGroupId)) { releaseGroupId = releaseResult.ReleaseGroupId; result.HasMetadata = true; @@ -157,13 +171,13 @@ namespace MediaBrowser.Providers.Music } // If we have a release Id but not a release group Id... - if (!string.IsNullOrEmpty(releaseId) && string.IsNullOrEmpty(releaseGroupId)) + if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) { - releaseGroupId = await GetReleaseGroupId(releaseId, cancellationToken).ConfigureAwait(false); + releaseGroupId = await GetReleaseGroupFromReleaseId(releaseId, cancellationToken).ConfigureAwait(false); result.HasMetadata = true; } - if (!string.IsNullOrEmpty(releaseId) || !string.IsNullOrEmpty(releaseGroupId)) + if (!string.IsNullOrWhiteSpace(releaseId) || !string.IsNullOrWhiteSpace(releaseGroupId)) { result.HasMetadata = true; } @@ -411,13 +425,42 @@ namespace MediaBrowser.Providers.Music } } + private async Task GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken) + { + var url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); + + using (var stream = await GetMusicBrainzResponse(url, true, true, cancellationToken).ConfigureAwait(false)) + { + using (var oReader = new StreamReader(stream, Encoding.UTF8)) + { + var settings = _xmlSettings.Create(false); + + settings.CheckCharacters = false; + settings.IgnoreProcessingInstructions = true; + settings.IgnoreComments = true; + + using (var reader = XmlReader.Create(oReader, settings)) + { + var result = ReleaseResult.Parse(reader).FirstOrDefault(); + + if (result != null) + { + return result.ReleaseId; + } + } + } + } + + return null; + } + /// /// Gets the release group id internal. /// /// The release entry id. /// The cancellation token. /// Task{System.String}. - private async Task GetReleaseGroupId(string releaseEntryId, CancellationToken cancellationToken) + private async Task GetReleaseGroupFromReleaseId(string releaseEntryId, CancellationToken cancellationToken) { var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId); @@ -514,11 +557,11 @@ namespace MediaBrowser.Providers.Music private List _mbzUrls = null; private MbzUrl _chosenUrl; - private async Task GetMbzUrl() + private async Task GetMbzUrl(bool forceMusicBrainzProper = false) { if (_chosenUrl == null || _mbzUrls == null || (DateTime.UtcNow.Ticks - _lastMbzUrlQueryTicks) > TimeSpan.FromHours(12).Ticks) { - var urls = await RefreshMzbUrls().ConfigureAwait(false); + var urls = await RefreshMzbUrls(forceMusicBrainzProper).ConfigureAwait(false); if (urls.Count > 1) { @@ -533,30 +576,12 @@ namespace MediaBrowser.Providers.Music return _chosenUrl; } - private async Task> RefreshMzbUrls() + private async Task> RefreshMzbUrls(bool forceMusicBrainzProper = false) { List list; - try + if (forceMusicBrainzProper) { - var options = new HttpRequestOptions - { - Url = "https://mb3admin.com/admin/service/standards/musicBrainzUrls", - UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion - }; - - using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) - { - var results = _json.DeserializeFromStream>(stream); - - list = results; - } - _lastMbzUrlQueryTicks = DateTime.UtcNow.Ticks; - } - catch (Exception ex) - { - _logger.ErrorException("Error getting music brainz info", ex); - list = new List { new MbzUrl @@ -566,22 +591,55 @@ namespace MediaBrowser.Providers.Music } }; } + else + { + try + { + var options = new HttpRequestOptions + { + Url = "https://mb3admin.com/admin/service/standards/musicBrainzUrls", + UserAgent = _appHost.Name + "/" + _appHost.ApplicationVersion + }; + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var results = _json.DeserializeFromStream>(stream); + + list = results; + } + _lastMbzUrlQueryTicks = DateTime.UtcNow.Ticks; + } + catch (Exception ex) + { + _logger.ErrorException("Error getting music brainz info", ex); + + list = new List + { + new MbzUrl + { + url = MusicBrainzBaseUrl, + throttleMs = 1000 + } + }; + } + } _mbzUrls = list.ToList(); return list; } + internal Task GetMusicBrainzResponse(string url, bool isSearch, CancellationToken cancellationToken) + { + return GetMusicBrainzResponse(url, isSearch, false, cancellationToken); + } + /// /// Gets the music brainz response. /// - /// The URL. - /// if set to true [is search]. - /// The cancellation token. - /// Task{XmlDocument}. - internal async Task GetMusicBrainzResponse(string url, bool isSearch, CancellationToken cancellationToken) + internal async Task GetMusicBrainzResponse(string url, bool isSearch, bool forceMusicBrainzProper, CancellationToken cancellationToken) { - var urlInfo = await GetMbzUrl().ConfigureAwait(false); + var urlInfo = await GetMbzUrl(forceMusicBrainzProper).ConfigureAwait(false); var throttleMs = urlInfo.throttleMs; if (throttleMs > 0) diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index 46be61486..4a52b972f 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -119,7 +119,7 @@ namespace MediaBrowser.Providers.TV if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) { - await TvdbSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); + await TvdbSeriesProvider.Current.EnsureSeriesInfo(searchInfo.SeriesProviderIds, null, null, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, searchInfo.SeriesProviderIds); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs index f6af365fd..72bd62d9f 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbPrescanTask.cs @@ -389,7 +389,7 @@ namespace MediaBrowser.Providers.TV _fileSystem.CreateDirectory(seriesDataPath); - return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); + return TvdbSeriesProvider.Current.DownloadSeriesZip(id, MetadataProviders.Tvdb.ToString(), null, null, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, cancellationToken); } } } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs index 2c4ccddd7..e68b7ad1d 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs @@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.TV var seriesProviderIds = series.ProviderIds; var seasonNumber = season.IndexNumber.Value; - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); + var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(seriesProviderIds, series.Name, series.ProductionYear, series.GetPreferredMetadataLanguage(), cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(seriesDataPath)) { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs index 97eedfa92..cdb9ac51e 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs @@ -70,7 +70,7 @@ namespace MediaBrowser.Providers.TV { var language = item.GetPreferredMetadataLanguage(); - var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(item.ProviderIds, language, cancellationToken).ConfigureAwait(false); + var seriesDataPath = await TvdbSeriesProvider.Current.EnsureSeriesInfo(item.ProviderIds, item.Name, item.ProductionYear, language, cancellationToken).ConfigureAwait(false); var path = Path.Combine(seriesDataPath, "banners.xml"); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index 44550d2be..c340d03f5 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.TV if (IsValidSeries(itemId.ProviderIds)) { - await EnsureSeriesInfo(itemId.ProviderIds, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); + await EnsureSeriesInfo(itemId.ProviderIds, itemId.Name, itemId.Year, itemId.MetadataLanguage, cancellationToken).ConfigureAwait(false); result.Item = new Series(); result.HasMetadata = true; @@ -160,19 +160,11 @@ namespace MediaBrowser.Providers.TV /// /// Downloads the series zip. /// - /// The series id. - /// Type of the identifier. - /// The series data path. - /// The last tv database update time. - /// The preferred metadata language. - /// The cancellation token. - /// Task. - /// seriesId - internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) + internal async Task DownloadSeriesZip(string seriesId, string idType, string seriesName, int? seriesYear, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, CancellationToken cancellationToken) { try { - await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + await DownloadSeriesZip(seriesId, idType, seriesName, seriesYear, seriesDataPath, lastTvDbUpdateTime, preferredMetadataLanguage, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); return; } catch (HttpException ex) @@ -185,11 +177,11 @@ namespace MediaBrowser.Providers.TV if (!string.Equals(preferredMetadataLanguage, "en", StringComparison.OrdinalIgnoreCase)) { - await DownloadSeriesZip(seriesId, idType, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + await DownloadSeriesZip(seriesId, idType, seriesName, seriesYear, seriesDataPath, lastTvDbUpdateTime, "en", preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); } } - private async Task DownloadSeriesZip(string seriesId, string idType, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken) + private async Task DownloadSeriesZip(string seriesId, string idType, string seriesName, int? seriesYear, string seriesDataPath, long? lastTvDbUpdateTime, string preferredMetadataLanguage, string saveAsMetadataLanguage, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(seriesId)) { @@ -201,6 +193,23 @@ namespace MediaBrowser.Providers.TV seriesId = await GetSeriesByRemoteId(seriesId, idType, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); } + // If searching by remote id came up empty, then do a regular search + if (string.IsNullOrWhiteSpace(seriesId) && !string.IsNullOrWhiteSpace(seriesName)) + { + var searchInfo = new SeriesInfo + { + Name = seriesName, + Year = seriesYear, + MetadataLanguage = preferredMetadataLanguage + }; + var results = await GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + var result = results.FirstOrDefault(); + if (result != null) + { + seriesId = result.GetProviderId(MetadataProviders.Tvdb); + } + } + if (string.IsNullOrWhiteSpace(seriesId)) { throw new ArgumentNullException("seriesId"); @@ -380,7 +389,7 @@ namespace MediaBrowser.Providers.TV } private SemaphoreSlim _ensureSemaphore = new SemaphoreSlim(1, 1); - internal async Task EnsureSeriesInfo(Dictionary seriesProviderIds, string preferredMetadataLanguage, CancellationToken cancellationToken) + internal async Task EnsureSeriesInfo(Dictionary seriesProviderIds, string seriesName, int? seriesYear, string preferredMetadataLanguage, CancellationToken cancellationToken) { await _ensureSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -395,7 +404,7 @@ namespace MediaBrowser.Providers.TV // The post-scan task will take care of updates so we don't need to re-download here if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) { - await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + await DownloadSeriesZip(seriesId, MetadataProviders.Tvdb.ToString(), seriesName, seriesYear, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); } return seriesDataPath; @@ -409,7 +418,7 @@ namespace MediaBrowser.Providers.TV // The post-scan task will take care of updates so we don't need to re-download here if (!IsCacheValid(seriesDataPath, preferredMetadataLanguage)) { - await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + await DownloadSeriesZip(seriesId, MetadataProviders.Imdb.ToString(), seriesName, seriesYear, seriesDataPath, null, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); } return seriesDataPath; diff --git a/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj b/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj index 4d9f3e7c4..bb67a9e36 100644 --- a/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj +++ b/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj @@ -386,14 +386,8 @@ Resources\dashboard-ui\livetvstatus.html - - Resources\dashboard-ui\livetvtunerprovider-hdhomerun.html - - - Resources\dashboard-ui\livetvtunerprovider-m3u.html - - - Resources\dashboard-ui\livetvtunerprovider-satip.html + + Resources\dashboard-ui\livetvtuner.html Resources\dashboard-ui\log.html @@ -560,12 +554,6 @@ Resources\dashboard-ui\wizardlibrary.html - - Resources\dashboard-ui\wizardlivetvguide.html - - - Resources\dashboard-ui\wizardlivetvtuner.html - Resources\dashboard-ui\wizardsettings.html @@ -800,6 +788,9 @@ Resources\dashboard-ui\bower_components\emby-webcomponents\shortcuts.js + + Resources\dashboard-ui\bower_components\emby-webcomponents\staticbackdrops.js + Resources\dashboard-ui\bower_components\emby-webcomponents\thememediaplayer.js @@ -1253,6 +1244,9 @@ Resources\dashboard-ui\bower_components\emby-webcomponents\recordingcreator\empty.png + + Resources\dashboard-ui\bower_components\emby-webcomponents\recordingcreator\recordingbutton.js + Resources\dashboard-ui\bower_components\emby-webcomponents\recordingcreator\recordingcreator.css @@ -1736,9 +1730,15 @@ Resources\dashboard-ui\components\iap.js + + Resources\dashboard-ui\components\maintabsmanager.js + Resources\dashboard-ui\components\remotecontrol.js + + Resources\dashboard-ui\components\tunerpicker.js + Resources\dashboard-ui\components\viewcontainer-lite.js @@ -2141,8 +2141,8 @@ Resources\dashboard-ui\dashboard\librarysettings.js - - Resources\dashboard-ui\dashboard\livetvtunerprovider-satip.js + + Resources\dashboard-ui\dashboard\livetvtuner.js Resources\dashboard-ui\dashboard\logpage.js @@ -2171,6 +2171,12 @@ Resources\dashboard-ui\legacy\selectmenu.js + + Resources\dashboard-ui\offline\offline.html + + + Resources\dashboard-ui\offline\offline.js + Resources\dashboard-ui\scripts\addpluginpage.js @@ -2189,9 +2195,6 @@ Resources\dashboard-ui\scripts\channels.js - - Resources\dashboard-ui\scripts\channelslatest.js - Resources\dashboard-ui\scripts\connectlogin.js @@ -2306,12 +2309,6 @@ Resources\dashboard-ui\scripts\livetvsuggested.js - - Resources\dashboard-ui\scripts\livetvtunerprovider-hdhomerun.js - - - Resources\dashboard-ui\scripts\livetvtunerprovider-m3u.js - Resources\dashboard-ui\scripts\localsync.js @@ -2516,12 +2513,6 @@ Resources\dashboard-ui\scripts\wizardcontroller.js - - Resources\dashboard-ui\scripts\wizardlivetvguide.js - - - Resources\dashboard-ui\scripts\wizardlivetvtuner.js - Resources\dashboard-ui\scripts\wizardsettings.js @@ -2576,6 +2567,9 @@ Resources\dashboard-ui\strings\es.json + + Resources\dashboard-ui\strings\fa.json + Resources\dashboard-ui\strings\fi.json diff --git a/MediaBrowser.Server.Mac/MacAppHost.cs b/MediaBrowser.Server.Mac/MacAppHost.cs index 59e72e71e..304472529 100644 --- a/MediaBrowser.Server.Mac/MacAppHost.cs +++ b/MediaBrowser.Server.Mac/MacAppHost.cs @@ -56,42 +56,6 @@ namespace MediaBrowser.Server.Mac return new SyncManager(); } - protected override FFMpegInstallInfo GetFfmpegInstallInfo() - { - var info = new FFMpegInstallInfo(); - - info.ArchiveType = "7z"; - - switch (EnvironmentInfo.SystemArchitecture) - { - case Architecture.X64: - info.Version = "20160124"; - break; - case Architecture.X86: - info.Version = "20150110"; - break; - } - - info.DownloadUrls = GetDownloadUrls(); - - return info; - } - - private string[] GetDownloadUrls() - { - switch (EnvironmentInfo.SystemArchitecture) - { - case Architecture.X64: - return new[] - { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/osx/ffmpeg-x64-2.8.5.7z" - }; - } - - // No version available - return new string[] { }; - } - protected override void RestartInternal() { MainClass.Restart(); @@ -123,10 +87,6 @@ namespace MediaBrowser.Server.Mac throw new NotImplementedException(); } - protected override void EnableLoopbackInternal(string appName) - { - } - public override bool SupportsRunningAsService { get diff --git a/MediaBrowser.Server.Mac/Main.cs b/MediaBrowser.Server.Mac/Main.cs index debd5f539..d703f7d0d 100644 --- a/MediaBrowser.Server.Mac/Main.cs +++ b/MediaBrowser.Server.Mac/Main.cs @@ -105,13 +105,12 @@ namespace MediaBrowser.Server.Mac // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); - var fileSystem = new MonoFileSystem(logManager.GetLogger("FileSystem"), false, false, appPaths.TempDirectory); - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); + var environmentInfo = GetEnvironmentInfo(); + + var fileSystem = new MonoFileSystem(logManager.GetLogger("FileSystem"), environmentInfo, appPaths.TempDirectory); _fileSystem = fileSystem; - var environmentInfo = GetEnvironmentInfo(); - var imageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, diff --git a/MediaBrowser.Server.Mac/Native/MonoFileSystem.cs b/MediaBrowser.Server.Mac/Native/MonoFileSystem.cs index daf2b90e6..7aeff5ac8 100644 --- a/MediaBrowser.Server.Mac/Native/MonoFileSystem.cs +++ b/MediaBrowser.Server.Mac/Native/MonoFileSystem.cs @@ -1,13 +1,14 @@ using Emby.Common.Implementations.IO; using MediaBrowser.Model.Logging; using Mono.Unix.Native; +using MediaBrowser.Model.System; namespace Emby.Server.Mac.Native { public class MonoFileSystem : ManagedFileSystem { - public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, string tempPath) - : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, true, tempPath) + public MonoFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath) + : base(logger, environmentInfo, tempPath) { } diff --git a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs index a5dc691a7..91c064efe 100644 --- a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs +++ b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs @@ -1,13 +1,14 @@ using Emby.Common.Implementations.IO; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; using Mono.Unix.Native; namespace MediaBrowser.Server.Mono.Native { public class MonoFileSystem : ManagedFileSystem { - public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, string tempPath) - : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, true, tempPath) + public MonoFileSystem(ILogger logger, IEnvironmentInfo environment, string tempPath) + : base(logger, environment, tempPath) { } diff --git a/MediaBrowser.Server.Mono/Program.cs b/MediaBrowser.Server.Mono/Program.cs index 649283410..66851f7e9 100644 --- a/MediaBrowser.Server.Mono/Program.cs +++ b/MediaBrowser.Server.Mono/Program.cs @@ -114,13 +114,12 @@ namespace MediaBrowser.Server.Mono // Allow all https requests ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; }); - var fileSystem = new MonoFileSystem(logManager.GetLogger("FileSystem"), false, false, appPaths.TempDirectory); - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); + var environmentInfo = GetEnvironmentInfo(); + + var fileSystem = new MonoFileSystem(logManager.GetLogger("FileSystem"), environmentInfo, appPaths.TempDirectory); FileSystem = fileSystem; - var environmentInfo = GetEnvironmentInfo(); - var imageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => _appHost.HttpClient, appPaths); _appHost = new MonoAppHost(appPaths, diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index b41e7607c..b02c5d6ac 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -331,9 +331,9 @@ namespace MediaBrowser.ServerApplication /// The options. private static void RunApplication(ServerApplicationPaths appPaths, ILogManager logManager, bool runService, StartupOptions options) { - var fileSystem = new ManagedFileSystem(logManager.GetLogger("FileSystem"), true, true, false, appPaths.TempDirectory); - fileSystem.AddShortcutHandler(new LnkShortcutHandler()); - fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); + var environmentInfo = new EnvironmentInfo(); + + var fileSystem = new ManagedFileSystem(logManager.GetLogger("FileSystem"), environmentInfo, appPaths.TempDirectory); var imageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => _appHost.HttpClient, appPaths); @@ -345,7 +345,7 @@ namespace MediaBrowser.ServerApplication fileSystem, new PowerManagement(), "emby.windows.zip", - new EnvironmentInfo(), + environmentInfo, imageEncoder, new Server.Startup.Common.SystemEvents(logManager.GetLogger("SystemEvents")), new RecyclableMemoryStreamProvider(), diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 7ebbc3809..b968c2fb6 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -142,8 +142,7 @@ MainForm.cs - - + diff --git a/MediaBrowser.ServerApplication/Native/LoopbackUtil.cs b/MediaBrowser.ServerApplication/Native/LoopUtil.cs similarity index 97% rename from MediaBrowser.ServerApplication/Native/LoopbackUtil.cs rename to MediaBrowser.ServerApplication/Native/LoopUtil.cs index 5b260685b..6160f853f 100644 --- a/MediaBrowser.ServerApplication/Native/LoopbackUtil.cs +++ b/MediaBrowser.ServerApplication/Native/LoopUtil.cs @@ -5,6 +5,9 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +/* + * Important - Even though this will compile in the shared projects, it will cause build failures within the mono runtime + */ namespace MediaBrowser.ServerApplication.Native { /// @@ -71,12 +74,12 @@ namespace MediaBrowser.ServerApplication.Native [DllImport("advapi32.dll", SetLastError = true)] internal static extern bool ConvertStringSidToSid(string strSid, out IntPtr pSid); - [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] + [DllImport("advapi32", /*CharSet = CharSet.Auto,*/ SetLastError = true)] static extern bool ConvertSidToStringSid( [MarshalAs(UnmanagedType.LPArray)] byte[] pSID, out IntPtr ptrSid); - [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] + [DllImport("advapi32", /*CharSet = CharSet.Auto,*/ SetLastError = true)] static extern bool ConvertSidToStringSid(IntPtr pSid, out string strSid); // Use this API to convert a string reference (e.g. "@{blah.pri?ms-resource://whatever}") into a plain string @@ -178,7 +181,7 @@ namespace MediaBrowser.ServerApplication.Native foreach (var app in Apps) { - if ((app.appContainerName ?? string.Empty).IndexOf(appName, StringComparison.OrdinalIgnoreCase) != -1 || + if ((app.appContainerName ?? string.Empty).IndexOf(appName, StringComparison.OrdinalIgnoreCase) != -1 || (app.displayName ?? string.Empty).IndexOf(appName, StringComparison.OrdinalIgnoreCase) != -1) { if (!app.LoopUtil) diff --git a/MediaBrowser.ServerApplication/WindowsAppHost.cs b/MediaBrowser.ServerApplication/WindowsAppHost.cs index cc899462a..8f1a88a74 100644 --- a/MediaBrowser.ServerApplication/WindowsAppHost.cs +++ b/MediaBrowser.ServerApplication/WindowsAppHost.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.InteropServices.ComTypes; +using Emby.Common.Implementations.IO; using Emby.Server.CinemaMode; using Emby.Server.Connect; using Emby.Server.Core; @@ -47,6 +48,11 @@ namespace MediaBrowser.ServerApplication MainStartup.Restart(); } + public override void EnableLoopback(string appName) + { + LoopUtil.Run(appName); + } + protected override List GetAssembliesWithPartsInternal() { var list = new List(); @@ -107,11 +113,6 @@ namespace MediaBrowser.ServerApplication } } - protected override void EnableLoopbackInternal(string appName) - { - LoopUtil.Run(appName); - } - public override bool SupportsRunningAsService { get diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 8a150e0c1..650d7a138 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -464,10 +464,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { - item.ProductionLocations = val.Split('/') + item.ProductionLocations.AddRange(val.Split('/') .Select(i => i.Trim()) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .ToList(); + .Where(i => !string.IsNullOrWhiteSpace(i))); } break; } diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 1e03f1169..54133b718 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.696 + 3.0.697 Emby.Common Emby Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index d6777dad1..e59460c94 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.696 + 3.0.697 Emby.Server.Core Emby Team ebr,Luke,scottisafool diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 99e3969aa..cc464e689 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -177,11 +177,15 @@ namespace Rssdp.Infrastructure { try { - await socket.SendAsync(messageData, messageData.Length, destination, cancellationToken).ConfigureAwait(false); + await socket.SendWithLockAsync(messageData, messageData.Length, destination, cancellationToken).ConfigureAwait(false); } catch (ObjectDisposedException) { + } + catch (OperationCanceledException) + { + } catch (Exception ex) { @@ -341,11 +345,9 @@ namespace Rssdp.Infrastructure foreach (var socket in sockets) { - await socket.SendAsync(messageData, messageData.Length, destination, cancellationToken).ConfigureAwait(false); + await SendFromSocket(socket, messageData, destination, cancellationToken).ConfigureAwait(false); } } - - ThrowIfDisposed(); } private ISocket ListenForBroadcastsAsync() diff --git a/SocketHttpListener.Portable/Net/EndPointListener.cs b/SocketHttpListener.Portable/Net/EndPointListener.cs index c7642d5d1..4f1a17fc0 100644 --- a/SocketHttpListener.Portable/Net/EndPointListener.cs +++ b/SocketHttpListener.Portable/Net/EndPointListener.cs @@ -32,8 +32,9 @@ namespace SocketHttpListener.Net private readonly ISocketFactory _socketFactory; private readonly ITextEncoding _textEncoding; private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly IFileSystem _fileSystem; - public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem) { this.listener = listener; _logger = logger; @@ -42,6 +43,7 @@ namespace SocketHttpListener.Net _socketFactory = socketFactory; _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; + _fileSystem = fileSystem; this.secure = secure; this.cert = cert; @@ -107,7 +109,7 @@ namespace SocketHttpListener.Net return; } - HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding).ConfigureAwait(false); + HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding, _fileSystem).ConfigureAwait(false); //_logger.Debug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); lock (listener.unregistered) diff --git a/SocketHttpListener.Portable/Net/EndPointManager.cs b/SocketHttpListener.Portable/Net/EndPointManager.cs index 797684b3e..11f774915 100644 --- a/SocketHttpListener.Portable/Net/EndPointManager.cs +++ b/SocketHttpListener.Portable/Net/EndPointManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Reflection; using System.Threading.Tasks; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using SocketHttpListener.Primitives; @@ -105,7 +106,7 @@ namespace SocketHttpListener.Net } else { - epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding); + epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding, listener.FileSystem); p[port] = epl; } diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs index 4a2cf3f5b..5fe47fc63 100644 --- a/SocketHttpListener.Portable/Net/HttpConnection.cs +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -35,13 +35,14 @@ namespace SocketHttpListener.Net ICertificate cert; Stream ssl_stream; - private ILogger _logger; + private readonly ILogger _logger; private readonly ICryptoProvider _cryptoProvider; private readonly IMemoryStreamFactory _memoryStreamFactory; private readonly ITextEncoding _textEncoding; private readonly IStreamFactory _streamFactory; + private readonly IFileSystem _fileSystem; - private HttpConnection(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + private HttpConnection(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem) { _logger = logger; this.sock = sock; @@ -51,6 +52,7 @@ namespace SocketHttpListener.Net _cryptoProvider = cryptoProvider; _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; + _fileSystem = fileSystem; _streamFactory = streamFactory; } @@ -82,9 +84,9 @@ namespace SocketHttpListener.Net Init(); } - public static async Task Create(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + public static async Task Create(ILogger logger, IAcceptSocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem) { - var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding); + var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding, fileSystem); await connection.InitStream().ConfigureAwait(false); @@ -121,7 +123,7 @@ namespace SocketHttpListener.Net position = 0; input_state = InputState.RequestLine; line_state = LineState.None; - context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding); + context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding, _fileSystem); } public bool IsClosed @@ -213,7 +215,9 @@ namespace SocketHttpListener.Net if (context.Response.SendChunked || isExpect100Continue || context.Request.IsWebSocketRequest || true) { - o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding); + var supportsDirectSocketAccess = !context.Response.SendChunked && !isExpect100Continue && !secure; + + o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding, _fileSystem, sock, supportsDirectSocketAccess); } else { diff --git a/SocketHttpListener.Portable/Net/HttpListener.cs b/SocketHttpListener.Portable/Net/HttpListener.cs index 2b0f75d01..c2e7acd8e 100644 --- a/SocketHttpListener.Portable/Net/HttpListener.cs +++ b/SocketHttpListener.Portable/Net/HttpListener.cs @@ -18,6 +18,7 @@ namespace SocketHttpListener.Net internal ICryptoProvider CryptoProvider { get; private set; } internal IStreamFactory StreamFactory { get; private set; } internal ISocketFactory SocketFactory { get; private set; } + internal IFileSystem FileSystem { get; private set; } internal ITextEncoding TextEncoding { get; private set; } internal IMemoryStreamFactory MemoryStreamFactory { get; private set; } internal INetworkManager NetworkManager { get; private set; } @@ -39,7 +40,7 @@ namespace SocketHttpListener.Net public Action OnContext { get; set; } - public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem) { _logger = logger; CryptoProvider = cryptoProvider; @@ -48,19 +49,20 @@ namespace SocketHttpListener.Net NetworkManager = networkManager; TextEncoding = textEncoding; MemoryStreamFactory = memoryStreamFactory; + FileSystem = fileSystem; prefixes = new HttpListenerPrefixCollection(logger, this); registry = new Dictionary(); connections = new Dictionary(); auth_schemes = AuthenticationSchemes.Anonymous; } - public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) - :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem) + :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory, fileSystem) { } - public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) - : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory, IFileSystem fileSystem) + : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory, fileSystem) { _certificate = certificate; } diff --git a/SocketHttpListener.Portable/Net/HttpListenerContext.cs b/SocketHttpListener.Portable/Net/HttpListenerContext.cs index 182fd2d2a..58d769f22 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerContext.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerContext.cs @@ -18,20 +18,18 @@ namespace SocketHttpListener.Net HttpConnection cnc; string error; int err_status = 400; - private readonly ILogger _logger; private readonly ICryptoProvider _cryptoProvider; private readonly IMemoryStreamFactory _memoryStreamFactory; private readonly ITextEncoding _textEncoding; - internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem) { this.cnc = cnc; - _logger = logger; _cryptoProvider = cryptoProvider; _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; request = new HttpListenerRequest(this, _textEncoding); - response = new HttpListenerResponse(this, _logger, _textEncoding); + response = new HttpListenerResponse(this, logger, _textEncoding, fileSystem); } internal int ErrorStatus diff --git a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs index 811cc6ddb..cfbd49203 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs @@ -456,7 +456,7 @@ namespace SocketHttpListener.Net public long ContentLength64 { - get { return content_length; } + get { return is_chunked ? -1 : content_length; } } public string ContentType diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs index 9a5862cb9..d9f91c0cc 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -3,6 +3,9 @@ using System.Globalization; using System.IO; using System.Net; using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Text; using SocketHttpListener.Primitives; @@ -32,12 +35,14 @@ namespace SocketHttpListener.Net private readonly ILogger _logger; private readonly ITextEncoding _textEncoding; + private readonly IFileSystem _fileSystem; - internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding) + internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding, IFileSystem fileSystem) { this.context = context; _logger = logger; _textEncoding = textEncoding; + _fileSystem = fileSystem; } internal bool CloseConnection @@ -366,7 +371,7 @@ namespace SocketHttpListener.Net { if (chunked) { - return ; + return; } Version v = context.Request.ProtocolVersion; @@ -509,5 +514,10 @@ namespace SocketHttpListener.Net cookies.Add(cookie); } + + public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + return ((ResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); + } } } \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs index a79a18791..3c79f47c2 100644 --- a/SocketHttpListener.Portable/Net/ResponseStream.cs +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Text; using SocketHttpListener.Primitives; @@ -22,12 +23,18 @@ namespace SocketHttpListener.Net Stream stream; private readonly IMemoryStreamFactory _memoryStreamFactory; private readonly ITextEncoding _textEncoding; + private readonly IFileSystem _fileSystem; + private readonly IAcceptSocket _socket; + private readonly bool _supportsDirectSocketAccess; - internal ResponseStream(Stream stream, HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + internal ResponseStream(Stream stream, HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding, IFileSystem fileSystem, IAcceptSocket socket, bool supportsDirectSocketAccess) { this.response = response; _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; + _fileSystem = fileSystem; + _socket = socket; + _supportsDirectSocketAccess = supportsDirectSocketAccess; this.stream = stream; } @@ -299,5 +306,80 @@ namespace SocketHttpListener.Net { throw new NotSupportedException(); } + + public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + //if (_supportsDirectSocketAccess && offset == 0 && count == 0 && !response.SendChunked) + //{ + // return TransmitFileOverSocket(path, offset, count, cancellationToken); + //} + return TransmitFileManaged(path, offset, count, fileShareMode, cancellationToken); + } + + private readonly byte[] _emptyBuffer = new byte[] { }; + private async Task TransmitFileOverSocket(string path, long offset, long count, CancellationToken cancellationToken) + { + MemoryStream ms = GetHeaders(response, _memoryStreamFactory, false); + + var buffer = new byte[] {}; + if (ms != null) + { + ms.Position = 0; + + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + buffer = msBuffer; + } + + await _socket.SendFile(path, buffer, _emptyBuffer, cancellationToken).ConfigureAwait(false); + } + + private async Task TransmitFileManaged(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + var chunked = response.SendChunked; + + if (!chunked) + { + await WriteAsync(_emptyBuffer, 0, 0, cancellationToken).ConfigureAwait(false); + } + + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, true)) + { + if (offset > 0) + { + fs.Position = offset; + } + + var targetStream = chunked ? this : stream; + + if (count > 0) + { + await CopyToInternalAsync(fs, targetStream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await fs.CopyToAsync(targetStream, 81920, cancellationToken).ConfigureAwait(false); + } + } + } + + private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) + { + var array = new byte[81920]; + int count; + while ((count = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + var bytesToCopy = Math.Min(count, copyLength); + + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false); + + copyLength -= bytesToCopy; + + if (copyLength <= 0) + { + break; + } + } + } } }