From 3eb4091808735858b01855d298226d239be464af Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 02:37:52 -0400 Subject: move additional classes to new server lib --- MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 77981b528..1f2a6255e 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -16,6 +16,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Controller; using MediaBrowser.Controller.IO; using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Threading; namespace MediaBrowser.Server.Implementations.IO { @@ -136,12 +137,12 @@ namespace MediaBrowser.Server.Implementations.IO private IServerConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; - private readonly IServerApplicationHost _appHost; + private readonly ITimerFactory _timerFactory; /// /// Initializes a new instance of the class. /// - public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost) + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ITimerFactory timerFactory) { if (taskManager == null) { @@ -153,7 +154,7 @@ namespace MediaBrowser.Server.Implementations.IO Logger = logManager.GetLogger(GetType().Name); ConfigurationManager = configurationManager; _fileSystem = fileSystem; - _appHost = appHost; + _timerFactory = timerFactory; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -528,7 +529,7 @@ namespace MediaBrowser.Server.Implementations.IO } } - var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger); + var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger, _timerFactory); newRefresher.Completed += NewRefresher_Completed; _activeRefreshers.Add(newRefresher); } -- cgit v1.2.3 From f52373609eac871c2883e1052020ff5327b19707 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 18:34:16 -0400 Subject: move classes to portable project --- .../Cryptography/CryptographyProvider.cs | 8 + .../Reflection/AssemblyInfo.cs | 18 + .../TextEncoding/TextEncoding.cs | 48 + .../Xml/XmlReaderSettingsFactory.cs | 22 + .../Emby.Server.Implementations.csproj | 21 +- Emby.Server.Implementations/IO/ThrottledStream.cs | 394 ++++++ .../Sync/AppSyncProvider.cs | 118 ++ .../Sync/CloudSyncProfile.cs | 302 +++++ .../Sync/IHasSyncQuality.cs | 31 + Emby.Server.Implementations/Sync/MediaSync.cs | 500 +++++++ .../Sync/MultiProviderSync.cs | 79 ++ .../Sync/ServerSyncScheduledTask.cs | 95 ++ Emby.Server.Implementations/Sync/SyncConfig.cs | 29 + .../Sync/SyncConvertScheduledTask.cs | 89 ++ Emby.Server.Implementations/Sync/SyncHelper.cs | 24 + Emby.Server.Implementations/Sync/SyncJobOptions.cs | 18 + .../Sync/SyncJobProcessor.cs | 988 ++++++++++++++ Emby.Server.Implementations/Sync/SyncManager.cs | 1362 ++++++++++++++++++++ .../Sync/SyncNotificationEntryPoint.cs | 48 + .../Sync/SyncRegistrationInfo.cs | 31 + .../Sync/SyncedMediaSourceProvider.cs | 158 +++ .../Sync/TargetDataProvider.cs | 188 +++ .../Subtitles/SubtitleEncoder.cs | 4 +- .../Cryptography/ICryptographyProvider.cs | 1 + .../IO/LibraryMonitor.cs | 2 +- .../IO/ThrottledStream.cs | 393 ------ .../MediaBrowser.Server.Implementations.csproj | 20 - .../Reflection/AssemblyInfo.cs | 14 - .../Sync/AppSyncProvider.cs | 118 -- .../Sync/CloudSyncProfile.cs | 302 ----- .../Sync/IHasSyncQuality.cs | 31 - .../Sync/MediaSync.cs | 502 -------- .../Sync/MultiProviderSync.cs | 76 -- .../Sync/ServerSyncScheduledTask.cs | 92 -- .../Sync/SyncConfig.cs | 29 - .../Sync/SyncConvertScheduledTask.cs | 89 -- .../Sync/SyncHelper.cs | 24 - .../Sync/SyncJobOptions.cs | 18 - .../Sync/SyncJobProcessor.cs | 988 -------------- .../Sync/SyncManager.cs | 1361 ------------------- .../Sync/SyncNotificationEntryPoint.cs | 48 - .../Sync/SyncRegistrationInfo.cs | 31 - .../Sync/SyncedMediaSourceProvider.cs | 158 --- .../Sync/TargetDataProvider.cs | 188 --- .../TextEncoding/TextEncoding.cs | 49 - .../Xml/XmlReaderSettingsFactory.cs | 20 - .../ApplicationHost.cs | 7 +- 47 files changed, 4577 insertions(+), 4559 deletions(-) create mode 100644 Emby.Common.Implementations/Reflection/AssemblyInfo.cs create mode 100644 Emby.Common.Implementations/TextEncoding/TextEncoding.cs create mode 100644 Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs create mode 100644 Emby.Server.Implementations/IO/ThrottledStream.cs create mode 100644 Emby.Server.Implementations/Sync/AppSyncProvider.cs create mode 100644 Emby.Server.Implementations/Sync/CloudSyncProfile.cs create mode 100644 Emby.Server.Implementations/Sync/IHasSyncQuality.cs create mode 100644 Emby.Server.Implementations/Sync/MediaSync.cs create mode 100644 Emby.Server.Implementations/Sync/MultiProviderSync.cs create mode 100644 Emby.Server.Implementations/Sync/ServerSyncScheduledTask.cs create mode 100644 Emby.Server.Implementations/Sync/SyncConfig.cs create mode 100644 Emby.Server.Implementations/Sync/SyncConvertScheduledTask.cs create mode 100644 Emby.Server.Implementations/Sync/SyncHelper.cs create mode 100644 Emby.Server.Implementations/Sync/SyncJobOptions.cs create mode 100644 Emby.Server.Implementations/Sync/SyncJobProcessor.cs create mode 100644 Emby.Server.Implementations/Sync/SyncManager.cs create mode 100644 Emby.Server.Implementations/Sync/SyncNotificationEntryPoint.cs create mode 100644 Emby.Server.Implementations/Sync/SyncRegistrationInfo.cs create mode 100644 Emby.Server.Implementations/Sync/SyncedMediaSourceProvider.cs create mode 100644 Emby.Server.Implementations/Sync/TargetDataProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/IO/ThrottledStream.cs delete mode 100644 MediaBrowser.Server.Implementations/Reflection/AssemblyInfo.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/MediaSync.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncConfig.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncHelper.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncManager.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncNotificationEntryPoint.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncRegistrationInfo.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs delete mode 100644 MediaBrowser.Server.Implementations/TextEncoding/TextEncoding.cs delete mode 100644 MediaBrowser.Server.Implementations/Xml/XmlReaderSettingsFactory.cs (limited to 'MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs') diff --git a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs index e5e4b1c7c..7b8d95b96 100644 --- a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs @@ -35,5 +35,13 @@ namespace Emby.Common.Implementations.Cryptography return provider.ComputeHash(str); } } + + public byte[] GetMD5Bytes(byte[] bytes) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(bytes); + } + } } } diff --git a/Emby.Common.Implementations/Reflection/AssemblyInfo.cs b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs new file mode 100644 index 000000000..820856da5 --- /dev/null +++ b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.IO; +using MediaBrowser.Model.Reflection; +using System.Reflection; + +namespace Emby.Common.Implementations.Reflection +{ + public class AssemblyInfo : IAssemblyInfo + { + public Stream GetManifestResourceStream(Type type, string resource) + { +#if NET46 + return type.Assembly.GetManifestResourceStream(resource); +#endif + return type.GetTypeInfo().Assembly.GetManifestResourceStream(resource); + } + } +} diff --git a/Emby.Common.Implementations/TextEncoding/TextEncoding.cs b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs new file mode 100644 index 000000000..35b869e43 --- /dev/null +++ b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs @@ -0,0 +1,48 @@ +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.TextEncoding; + +namespace Emby.Common.Implementations.TextEncoding +{ + public class TextEncoding : IEncoding + { + private readonly IFileSystem _fileSystem; + + public TextEncoding(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public byte[] GetASCIIBytes(string text) + { + return Encoding.ASCII.GetBytes(text); + } + + public string GetASCIIString(byte[] bytes, int startIndex, int length) + { + return Encoding.ASCII.GetString(bytes, 0, bytes.Length); + } + + public Encoding GetFileEncoding(string srcFile) + { + // *** Detect byte order mark if any - otherwise assume default + var buffer = new byte[5]; + + using (var file = _fileSystem.OpenRead(srcFile)) + { + file.Read(buffer, 0, 5); + } + + if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) + return Encoding.UTF8; + if (buffer[0] == 0xfe && buffer[1] == 0xff) + return Encoding.Unicode; + if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff) + return Encoding.UTF32; + if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76) + return Encoding.UTF7; + + return null; + } + } +} diff --git a/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs new file mode 100644 index 000000000..806290cf4 --- /dev/null +++ b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs @@ -0,0 +1,22 @@ +using System.Xml; +using MediaBrowser.Model.Xml; + +namespace Emby.Common.Implementations.Xml +{ + public class XmlReaderSettingsFactory : IXmlReaderSettingsFactory + { + public XmlReaderSettings Create(bool enableValidation) + { + var settings = new XmlReaderSettings(); + + if (!enableValidation) + { +#if NET46 + settings.ValidationType = ValidationType.None; +#endif + } + + return settings; + } + } +} diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index e3d404644..152a165e2 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -69,6 +69,7 @@ + @@ -161,14 +162,28 @@ + + + + + + + + + + + + + + + + - - - + {9142eefa-7570-41e1-bfcc-468bb571af2f} diff --git a/Emby.Server.Implementations/IO/ThrottledStream.cs b/Emby.Server.Implementations/IO/ThrottledStream.cs new file mode 100644 index 000000000..81760b639 --- /dev/null +++ b/Emby.Server.Implementations/IO/ThrottledStream.cs @@ -0,0 +1,394 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.IO +{ + /// + /// Class for streaming data with throttling support. + /// + public class ThrottledStream : Stream + { + /// + /// A constant used to specify an infinite number of bytes that can be transferred per second. + /// + public const long Infinite = 0; + + #region Private members + /// + /// The base stream. + /// + private readonly Stream _baseStream; + + /// + /// The maximum bytes per second that can be transferred through the base stream. + /// + private long _maximumBytesPerSecond; + + /// + /// The number of bytes that has been transferred since the last throttle. + /// + private long _byteCount; + + /// + /// The start time in milliseconds of the last throttle. + /// + private long _start; + #endregion + + #region Properties + /// + /// Gets the current milliseconds. + /// + /// The current milliseconds. + protected long CurrentMilliseconds + { + get + { + return Environment.TickCount; + } + } + + /// + /// Gets or sets the maximum bytes per second that can be transferred through the base stream. + /// + /// The maximum bytes per second. + public long MaximumBytesPerSecond + { + get + { + return _maximumBytesPerSecond; + } + set + { + if (MaximumBytesPerSecond != value) + { + _maximumBytesPerSecond = value; + Reset(); + } + } + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return _baseStream.CanRead; + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return _baseStream.CanSeek; + } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return _baseStream.CanWrite; + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// + /// A long value representing the length of the stream in bytes. + /// The base stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Length + { + get + { + return _baseStream.Length; + } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// + /// The current position within the stream. + /// An I/O error occurs. + /// The base stream does not support seeking. + /// Methods were called after the stream was closed. + public override long Position + { + get + { + return _baseStream.Position; + } + set + { + _baseStream.Position = value; + } + } + #endregion + + public long MinThrottlePosition; + + #region Ctor + /// + /// Initializes a new instance of the class. + /// + /// The base stream. + /// The maximum bytes per second that can be transferred through the base stream. + /// Thrown when is a null reference. + /// Thrown when is a negative value. + public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) + { + if (baseStream == null) + { + throw new ArgumentNullException("baseStream"); + } + + if (maximumBytesPerSecond < 0) + { + throw new ArgumentOutOfRangeException("maximumBytesPerSecond", + maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); + } + + _baseStream = baseStream; + _maximumBytesPerSecond = maximumBytesPerSecond; + _start = CurrentMilliseconds; + _byteCount = 0; + } + #endregion + + #region Public methods + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + /// An I/O error occurs. + public override void Flush() + { + _baseStream.Flush(); + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of offset and count is larger than the buffer length. + /// Methods were called after the stream was closed. + /// The base stream does not support reading. + /// buffer is null. + /// An I/O error occurs. + /// offset or count is negative. + public override int Read(byte[] buffer, int offset, int count) + { + Throttle(count); + + return _baseStream.Read(buffer, offset, count); + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + return _baseStream.Seek(offset, origin); + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + _baseStream.SetLength(value); + } + + private long _bytesWritten; + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// An I/O error occurs. + /// The base stream does not support writing. + /// Methods were called after the stream was closed. + /// buffer is null. + /// The sum of offset and count is greater than the buffer length. + /// offset or count is negative. + public override void Write(byte[] buffer, int offset, int count) + { + Throttle(count); + + _baseStream.Write(buffer, offset, count); + + _bytesWritten += count; + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); + + await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + _bytesWritten += count; + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return _baseStream.ToString(); + } + #endregion + + private bool ThrottleCheck(int bufferSizeInBytes) + { + if (_bytesWritten < MinThrottlePosition) + { + return false; + } + + // Make sure the buffer isn't empty. + if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) + { + return false; + } + + return true; + } + + #region Protected methods + /// + /// Throttles for the specified buffer size in bytes. + /// + /// The buffer size in bytes. + protected void Throttle(int bufferSizeInBytes) + { + if (!ThrottleCheck(bufferSizeInBytes)) + { + return ; + } + + _byteCount += bufferSizeInBytes; + long elapsedMilliseconds = CurrentMilliseconds - _start; + + if (elapsedMilliseconds > 0) + { + // Calculate the current bps. + long bps = _byteCount * 1000L / elapsedMilliseconds; + + // If the bps are more then the maximum bps, try to throttle. + if (bps > _maximumBytesPerSecond) + { + // Calculate the time to sleep. + long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; + int toSleep = (int)(wakeElapsed - elapsedMilliseconds); + + if (toSleep > 1) + { + try + { + // The time to sleep is more then a millisecond, so sleep. + var task = Task.Delay(toSleep); + Task.WaitAll(task); + } + catch + { + // Eatup ThreadAbortException. + } + + // A sleep has been done, reset. + Reset(); + } + } + } + } + + protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) + { + if (!ThrottleCheck(bufferSizeInBytes)) + { + return; + } + + _byteCount += bufferSizeInBytes; + long elapsedMilliseconds = CurrentMilliseconds - _start; + + if (elapsedMilliseconds > 0) + { + // Calculate the current bps. + long bps = _byteCount * 1000L / elapsedMilliseconds; + + // If the bps are more then the maximum bps, try to throttle. + if (bps > _maximumBytesPerSecond) + { + // Calculate the time to sleep. + long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; + int toSleep = (int)(wakeElapsed - elapsedMilliseconds); + + if (toSleep > 1) + { + // The time to sleep is more then a millisecond, so sleep. + await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); + + // A sleep has been done, reset. + Reset(); + } + } + } + } + + /// + /// Will reset the bytecount to 0 and reset the start time to the current time. + /// + protected void Reset() + { + long difference = CurrentMilliseconds - _start; + + // Only reset counters when a known history is available of more then 1 second. + if (difference > 1000) + { + _byteCount = 0; + _start = CurrentMilliseconds; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Sync/AppSyncProvider.cs b/Emby.Server.Implementations/Sync/AppSyncProvider.cs new file mode 100644 index 000000000..d405a0ff9 --- /dev/null +++ b/Emby.Server.Implementations/Sync/AppSyncProvider.cs @@ -0,0 +1,118 @@ +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Server.Implementations.Sync +{ + public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds, IHasSyncQuality, IHasDuplicateCheck + { + private readonly IDeviceManager _deviceManager; + + public AppSyncProvider(IDeviceManager deviceManager) + { + _deviceManager = deviceManager; + } + + public IEnumerable GetSyncTargets(string userId) + { + return _deviceManager.GetDevices(new DeviceQuery + { + SupportsSync = true, + UserId = userId + + }).Items.Select(i => new SyncTarget + { + Id = i.Id, + Name = i.Name + }); + } + + public DeviceProfile GetDeviceProfile(SyncTarget target, string profile, string quality) + { + var caps = _deviceManager.GetCapabilities(target.Id); + + var deviceProfile = caps == null || caps.DeviceProfile == null ? new DeviceProfile() : caps.DeviceProfile; + deviceProfile.MaxStaticBitrate = SyncHelper.AdjustBitrate(deviceProfile.MaxStaticBitrate, quality); + + return deviceProfile; + } + + public string Name + { + get { return "Mobile Sync"; } + } + + public IEnumerable GetAllSyncTargets() + { + return _deviceManager.GetDevices(new DeviceQuery + { + SupportsSync = true + + }).Items.Select(i => new SyncTarget + { + Id = i.Id, + Name = i.Name + }); + } + + public IEnumerable GetQualityOptions(SyncTarget target) + { + return new List + { + new SyncQualityOption + { + Name = "Original", + Id = "original", + Description = "Syncs original files as-is, regardless of whether the device is capable of playing them or not." + }, + new SyncQualityOption + { + Name = "High", + Id = "high", + IsDefault = true + }, + new SyncQualityOption + { + Name = "Medium", + Id = "medium" + }, + new SyncQualityOption + { + Name = "Low", + Id = "low" + }, + new SyncQualityOption + { + Name = "Custom", + Id = "custom" + } + }; + } + + public IEnumerable GetProfileOptions(SyncTarget target) + { + return new List(); + } + + public SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality) + { + var isConverting = !string.Equals(quality, "original", StringComparison.OrdinalIgnoreCase); + + return new SyncJobOptions + { + DeviceProfile = GetDeviceProfile(target, profile, quality), + IsConverting = isConverting + }; + } + + public bool AllowDuplicateJobItem(SyncJobItem original, SyncJobItem duplicate) + { + return false; + } + } +} diff --git a/Emby.Server.Implementations/Sync/CloudSyncProfile.cs b/Emby.Server.Implementations/Sync/CloudSyncProfile.cs new file mode 100644 index 000000000..1a78c8ae6 --- /dev/null +++ b/Emby.Server.Implementations/Sync/CloudSyncProfile.cs @@ -0,0 +1,302 @@ +using MediaBrowser.Model.Dlna; +using System.Collections.Generic; + +namespace Emby.Server.Implementations.Sync +{ + public class CloudSyncProfile : DeviceProfile + { + public CloudSyncProfile(bool supportsAc3, bool supportsDca) + { + Name = "Cloud Sync"; + + MaxStreamingBitrate = 20000000; + MaxStaticBitrate = 20000000; + + var mkvAudio = "aac,mp3"; + var mp4Audio = "aac"; + + if (supportsAc3) + { + mkvAudio += ",ac3"; + mp4Audio += ",ac3"; + } + + if (supportsDca) + { + mkvAudio += ",dca,dts"; + } + + var videoProfile = "high|main|baseline|constrained baseline"; + var videoLevel = "40"; + + DirectPlayProfiles = new[] + { + //new DirectPlayProfile + //{ + // Container = "mkv", + // VideoCodec = "h264,mpeg4", + // AudioCodec = mkvAudio, + // Type = DlnaProfileType.Video + //}, + new DirectPlayProfile + { + Container = "mp4,mov,m4v", + VideoCodec = "h264,mpeg4", + AudioCodec = mp4Audio, + Type = DlnaProfileType.Video + }, + new DirectPlayProfile + { + Container = "mp3", + Type = DlnaProfileType.Audio + } + }; + + ContainerProfiles = new[] + { + new ContainerProfile + { + Type = DlnaProfileType.Video, + Conditions = new [] + { + new ProfileCondition + { + Condition = ProfileConditionType.NotEquals, + Property = ProfileConditionValue.NumAudioStreams, + Value = "0", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.EqualsAny, + Property = ProfileConditionValue.NumVideoStreams, + Value = "1", + IsRequired = false + } + } + } + }; + + var codecProfiles = new List + { + new CodecProfile + { + Type = CodecType.Video, + Codec = "h264", + Conditions = new [] + { + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoBitDepth, + Value = "8", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.Width, + Value = "1920", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.Height, + Value = "1080", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.RefFrames, + Value = "4", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoFramerate, + Value = "30", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.Equals, + Property = ProfileConditionValue.IsAnamorphic, + Value = "false", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoLevel, + Value = videoLevel, + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.EqualsAny, + Property = ProfileConditionValue.VideoProfile, + Value = videoProfile, + IsRequired = false + } + } + }, + new CodecProfile + { + Type = CodecType.Video, + Codec = "mpeg4", + Conditions = new [] + { + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoBitDepth, + Value = "8", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.Width, + Value = "1920", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.Height, + Value = "1080", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.RefFrames, + Value = "4", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoFramerate, + Value = "30", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.Equals, + Property = ProfileConditionValue.IsAnamorphic, + Value = "false", + IsRequired = false + } + } + } + }; + + codecProfiles.Add(new CodecProfile + { + Type = CodecType.VideoAudio, + Codec = "ac3", + Conditions = new[] + { + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.AudioChannels, + Value = "6", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.AudioBitrate, + Value = "320000", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.Equals, + Property = ProfileConditionValue.IsSecondaryAudio, + Value = "false", + IsRequired = false + } + } + }); + codecProfiles.Add(new CodecProfile + { + Type = CodecType.VideoAudio, + Codec = "aac,mp3", + Conditions = new[] + { + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.AudioChannels, + Value = "2", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.AudioBitrate, + Value = "320000", + IsRequired = true + }, + new ProfileCondition + { + Condition = ProfileConditionType.Equals, + Property = ProfileConditionValue.IsSecondaryAudio, + Value = "false", + IsRequired = false + } + } + }); + + CodecProfiles = codecProfiles.ToArray(); + + SubtitleProfiles = new[] + { + new SubtitleProfile + { + Format = "srt", + Method = SubtitleDeliveryMethod.External + }, + new SubtitleProfile + { + Format = "vtt", + Method = SubtitleDeliveryMethod.External + } + }; + + TranscodingProfiles = new[] + { + new TranscodingProfile + { + Container = "mp3", + AudioCodec = "mp3", + Type = DlnaProfileType.Audio, + Context = EncodingContext.Static + }, + + new TranscodingProfile + { + Container = "mp4", + Type = DlnaProfileType.Video, + AudioCodec = "aac", + VideoCodec = "h264", + Context = EncodingContext.Static + }, + + new TranscodingProfile + { + Container = "jpeg", + Type = DlnaProfileType.Photo, + Context = EncodingContext.Static + } + }; + + } + } +} diff --git a/Emby.Server.Implementations/Sync/IHasSyncQuality.cs b/Emby.Server.Implementations/Sync/IHasSyncQuality.cs new file mode 100644 index 000000000..bec8b37a7 --- /dev/null +++ b/Emby.Server.Implementations/Sync/IHasSyncQuality.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace Emby.Server.Implementations.Sync +{ + public interface IHasSyncQuality + { + /// + /// Gets the device profile. + /// + /// The target. + /// The profile. + /// The quality. + /// DeviceProfile. + SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality); + + /// + /// Gets the quality options. + /// + /// The target. + /// IEnumerable<SyncQualityOption>. + IEnumerable GetQualityOptions(SyncTarget target); + + /// + /// Gets the profile options. + /// + /// The target. + /// IEnumerable<SyncQualityOption>. + IEnumerable GetProfileOptions(SyncTarget target); + } +} diff --git a/Emby.Server.Implementations/Sync/MediaSync.cs b/Emby.Server.Implementations/Sync/MediaSync.cs new file mode 100644 index 000000000..b420a3df4 --- /dev/null +++ b/Emby.Server.Implementations/Sync/MediaSync.cs @@ -0,0 +1,500 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.IO; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Sync +{ + public class MediaSync + { + private readonly ISyncManager _syncManager; + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IConfigurationManager _config; + private readonly ICryptographyProvider _cryptographyProvider; + + public const string PathSeparatorString = "/"; + public const char PathSeparatorChar = '/'; + + public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem, IConfigurationManager config, ICryptographyProvider cryptographyProvider) + { + _logger = logger; + _syncManager = syncManager; + _appHost = appHost; + _fileSystem = fileSystem; + _config = config; + _cryptographyProvider = cryptographyProvider; + } + + public async Task Sync(IServerSyncProvider provider, + ISyncDataProvider dataProvider, + SyncTarget target, + IProgress progress, + CancellationToken cancellationToken) + { + var serverId = _appHost.SystemId; + var serverName = _appHost.FriendlyName; + + await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); + progress.Report(3); + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(pct => + { + var totalProgress = pct * .97; + totalProgress += 1; + progress.Report(totalProgress); + }); + await GetNewMedia(provider, dataProvider, target, serverId, serverName, innerProgress, cancellationToken); + + // Do the data sync twice so the server knows what was removed from the device + await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); + + progress.Report(100); + } + + private async Task SyncData(IServerSyncProvider provider, + ISyncDataProvider dataProvider, + string serverId, + SyncTarget target, + CancellationToken cancellationToken) + { + var localItems = await dataProvider.GetLocalItems(target, serverId).ConfigureAwait(false); + var remoteFiles = await provider.GetFiles(target, cancellationToken).ConfigureAwait(false); + var remoteIds = remoteFiles.Items.Select(i => i.FullName).ToList(); + + var jobItemIds = new List(); + + foreach (var localItem in localItems) + { + if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase)) + { + jobItemIds.Add(localItem.SyncJobItemId); + } + } + + var result = await _syncManager.SyncData(new SyncDataRequest + { + TargetId = target.Id, + SyncJobItemIds = jobItemIds + + }).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + foreach (var itemIdToRemove in result.ItemIdsToRemove) + { + try + { + await RemoveItem(provider, dataProvider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove); + } + } + } + + private async Task GetNewMedia(IServerSyncProvider provider, + ISyncDataProvider dataProvider, + SyncTarget target, + string serverId, + string serverName, + IProgress progress, + CancellationToken cancellationToken) + { + var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false); + + var numComplete = 0; + double startingPercent = 0; + double percentPerItem = 1; + if (jobItems.Count > 0) + { + percentPerItem /= jobItems.Count; + } + + foreach (var jobItem in jobItems) + { + cancellationToken.ThrowIfCancellationRequested(); + + var currentPercent = startingPercent; + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(pct => + { + var totalProgress = pct * percentPerItem; + totalProgress += currentPercent; + progress.Report(totalProgress); + }); + + try + { + await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error syncing item", ex); + } + + numComplete++; + startingPercent = numComplete; + startingPercent /= jobItems.Count; + startingPercent *= 100; + progress.Report(startingPercent); + } + } + + private async Task GetItem(IServerSyncProvider provider, + ISyncDataProvider dataProvider, + SyncTarget target, + string serverId, + string serverName, + SyncedItem jobItem, + IProgress progress, + CancellationToken cancellationToken) + { + var libraryItem = jobItem.Item; + var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId); + var internalSyncJob = _syncManager.GetJob(jobItem.SyncJobId); + + var localItem = CreateLocalItem(provider, jobItem, internalSyncJob, target, libraryItem, serverId, serverName, jobItem.OriginalFileName); + + await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id); + + var transferSuccess = false; + Exception transferException = null; + + var options = _config.GetSyncOptions(); + + try + { + var fileTransferProgress = new ActionableProgress(); + fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92)); + + var sendFileResult = await SendFile(provider, internalSyncJobItem.OutputPath, localItem.LocalPath.Split(PathSeparatorChar), target, options, fileTransferProgress, cancellationToken).ConfigureAwait(false); + + if (localItem.Item.MediaSources != null) + { + var mediaSource = localItem.Item.MediaSources.FirstOrDefault(); + if (mediaSource != null) + { + mediaSource.Path = sendFileResult.Path; + mediaSource.Protocol = sendFileResult.Protocol; + mediaSource.RequiredHttpHeaders = sendFileResult.RequiredHttpHeaders; + mediaSource.SupportsTranscoding = false; + } + } + + localItem.FileId = sendFileResult.Id; + + // Create db record + await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); + + if (localItem.Item.MediaSources != null) + { + var mediaSource = localItem.Item.MediaSources.FirstOrDefault(); + if (mediaSource != null) + { + await SendSubtitles(localItem, mediaSource, provider, dataProvider, target, options, cancellationToken).ConfigureAwait(false); + } + } + + progress.Report(92); + + transferSuccess = true; + + progress.Report(99); + } + catch (Exception ex) + { + _logger.ErrorException("Error transferring sync job file", ex); + transferException = ex; + } + + if (transferSuccess) + { + await _syncManager.ReportSyncJobItemTransferred(jobItem.SyncJobItemId).ConfigureAwait(false); + } + else + { + await _syncManager.ReportSyncJobItemTransferFailed(jobItem.SyncJobItemId).ConfigureAwait(false); + + throw transferException; + } + } + + private async Task SendSubtitles(LocalItem localItem, MediaSourceInfo mediaSource, IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, SyncOptions options, CancellationToken cancellationToken) + { + var failedSubtitles = new List(); + var requiresSave = false; + + foreach (var mediaStream in mediaSource.MediaStreams + .Where(i => i.Type == MediaStreamType.Subtitle && i.IsExternal) + .ToList()) + { + try + { + var remotePath = GetRemoteSubtitlePath(localItem, mediaStream, provider, target); + var sendFileResult = await SendFile(provider, mediaStream.Path, remotePath, target, options, new Progress(), cancellationToken).ConfigureAwait(false); + + // This is the path that will be used when talking to the provider + mediaStream.ExternalId = sendFileResult.Id; + + // Keep track of all additional files for cleanup later. + localItem.AdditionalFiles.Add(sendFileResult.Id); + + // This is the public path clients will use + mediaStream.Path = sendFileResult.Path; + requiresSave = true; + } + catch (Exception ex) + { + _logger.ErrorException("Error sending subtitle stream", ex); + failedSubtitles.Add(mediaStream); + } + } + + if (failedSubtitles.Count > 0) + { + mediaSource.MediaStreams = mediaSource.MediaStreams.Except(failedSubtitles).ToList(); + requiresSave = true; + } + + if (requiresSave) + { + await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); + } + } + + private string[] GetRemoteSubtitlePath(LocalItem item, MediaStream stream, IServerSyncProvider provider, SyncTarget target) + { + var filename = GetSubtitleSaveFileName(item, stream.Language, stream.IsForced) + "." + stream.Codec.ToLower(); + + var pathParts = item.LocalPath.Split(PathSeparatorChar); + var list = pathParts.Take(pathParts.Length - 1).ToList(); + list.Add(filename); + + return list.ToArray(); + } + + private string GetSubtitleSaveFileName(LocalItem item, string language, bool isForced) + { + var path = item.LocalPath; + + var name = Path.GetFileNameWithoutExtension(path); + + if (!string.IsNullOrWhiteSpace(language)) + { + name += "." + language.ToLower(); + } + + if (isForced) + { + name += ".foreign"; + } + + return name; + } + + private async Task RemoveItem(IServerSyncProvider provider, + ISyncDataProvider dataProvider, + string serverId, + string syncJobItemId, + SyncTarget target, + CancellationToken cancellationToken) + { + var localItems = await dataProvider.GetItemsBySyncJobItemId(target, serverId, syncJobItemId); + + foreach (var localItem in localItems) + { + var files = localItem.AdditionalFiles.ToList(); + + foreach (var file in files) + { + _logger.Debug("Removing {0} from {1}.", file, target.Name); + await provider.DeleteFile(file, target, cancellationToken).ConfigureAwait(false); + } + + _logger.Debug("Removing {0} from {1}.", localItem.FileId, target.Name); + await provider.DeleteFile(localItem.FileId, target, cancellationToken).ConfigureAwait(false); + + await dataProvider.Delete(target, localItem.Id).ConfigureAwait(false); + } + } + + private async Task SendFile(IServerSyncProvider provider, string inputPath, string[] pathParts, SyncTarget target, SyncOptions options, IProgress progress, CancellationToken cancellationToken) + { + _logger.Debug("Sending {0} to {1}. Remote path: {2}", inputPath, provider.Name, string.Join("/", pathParts)); + var supportsDirectCopy = provider as ISupportsDirectCopy; + if (supportsDirectCopy != null) + { + return await supportsDirectCopy.SendFile(inputPath, pathParts, target, progress, cancellationToken).ConfigureAwait(false); + } + + using (var fileStream = _fileSystem.GetFileStream(inputPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) + { + Stream stream = fileStream; + + if (options.UploadSpeedLimitBytes > 0 && provider is IRemoteSyncProvider) + { + stream = new ThrottledStream(stream, options.UploadSpeedLimitBytes); + } + + return await provider.SendFile(stream, pathParts, target, progress, cancellationToken).ConfigureAwait(false); + } + } + + private string GetLocalId(string jobItemId, string itemId) + { + var bytes = Encoding.UTF8.GetBytes(jobItemId + itemId); + bytes = CreateMd5(bytes); + return BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty); + } + + private byte[] CreateMd5(byte[] value) + { + return _cryptographyProvider.GetMD5Bytes(value); + } + + public LocalItem CreateLocalItem(IServerSyncProvider provider, SyncedItem syncedItem, SyncJob job, SyncTarget target, BaseItemDto libraryItem, string serverId, string serverName, string originalFileName) + { + var path = GetDirectoryPath(provider, job, syncedItem, libraryItem, serverName); + path.Add(GetLocalFileName(provider, libraryItem, originalFileName)); + + var localPath = string.Join(PathSeparatorString, path.ToArray()); + + foreach (var mediaSource in libraryItem.MediaSources) + { + mediaSource.Path = localPath; + mediaSource.Protocol = MediaProtocol.File; + } + + return new LocalItem + { + Item = libraryItem, + ItemId = libraryItem.Id, + ServerId = serverId, + LocalPath = localPath, + Id = GetLocalId(syncedItem.SyncJobItemId, libraryItem.Id), + SyncJobItemId = syncedItem.SyncJobItemId + }; + } + + private List GetDirectoryPath(IServerSyncProvider provider, SyncJob job, SyncedItem syncedItem, BaseItemDto item, string serverName) + { + var parts = new List + { + serverName + }; + + var profileOption = _syncManager.GetProfileOptions(job.TargetId) + .FirstOrDefault(i => string.Equals(i.Id, job.Profile, StringComparison.OrdinalIgnoreCase)); + + string name; + + if (profileOption != null && !string.IsNullOrWhiteSpace(profileOption.Name)) + { + name = profileOption.Name; + + if (job.Bitrate.HasValue) + { + name += "-" + job.Bitrate.Value.ToString(CultureInfo.InvariantCulture); + } + else + { + var qualityOption = _syncManager.GetQualityOptions(job.TargetId) + .FirstOrDefault(i => string.Equals(i.Id, job.Quality, StringComparison.OrdinalIgnoreCase)); + + if (qualityOption != null && !string.IsNullOrWhiteSpace(qualityOption.Name)) + { + name += "-" + qualityOption.Name; + } + } + } + else + { + name = syncedItem.SyncJobName + "-" + syncedItem.SyncJobDateCreated + .ToLocalTime() + .ToString("g") + .Replace(" ", "-"); + } + + name = GetValidFilename(provider, name); + parts.Add(name); + + if (item.IsType("episode")) + { + parts.Add("TV"); + if (!string.IsNullOrWhiteSpace(item.SeriesName)) + { + parts.Add(item.SeriesName); + } + } + else if (item.IsVideo) + { + parts.Add("Videos"); + parts.Add(item.Name); + } + else if (item.IsAudio) + { + parts.Add("Music"); + + if (!string.IsNullOrWhiteSpace(item.AlbumArtist)) + { + parts.Add(item.AlbumArtist); + } + + if (!string.IsNullOrWhiteSpace(item.Album)) + { + parts.Add(item.Album); + } + } + else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) + { + parts.Add("Photos"); + + if (!string.IsNullOrWhiteSpace(item.Album)) + { + parts.Add(item.Album); + } + } + + return parts.Select(i => GetValidFilename(provider, i)).ToList(); + } + + private string GetLocalFileName(IServerSyncProvider provider, BaseItemDto item, string originalFileName) + { + var filename = originalFileName; + + if (string.IsNullOrWhiteSpace(filename)) + { + filename = item.Name; + } + + return GetValidFilename(provider, filename); + } + + private string GetValidFilename(IServerSyncProvider provider, string filename) + { + // We can always add this method to the sync provider if it's really needed + return _fileSystem.GetValidFilename(filename); + } + } +} diff --git a/Emby.Server.Implementations/Sync/MultiProviderSync.cs b/Emby.Server.Implementations/Sync/MultiProviderSync.cs new file mode 100644 index 000000000..db6cfcbd6 --- /dev/null +++ b/Emby.Server.Implementations/Sync/MultiProviderSync.cs @@ -0,0 +1,79 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Sync +{ + public class MultiProviderSync + { + private readonly SyncManager _syncManager; + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IConfigurationManager _config; + private readonly ICryptographyProvider _cryptographyProvider; + + public MultiProviderSync(SyncManager syncManager, IServerApplicationHost appHost, ILogger logger, IFileSystem fileSystem, IConfigurationManager config, ICryptographyProvider cryptographyProvider) + { + _syncManager = syncManager; + _appHost = appHost; + _logger = logger; + _fileSystem = fileSystem; + _config = config; + _cryptographyProvider = cryptographyProvider; + } + + public async Task Sync(IEnumerable providers, IProgress progress, CancellationToken cancellationToken) + { + var targets = providers + .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple(i, t))) + .ToList(); + + var numComplete = 0; + double startingPercent = 0; + double percentPerItem = 1; + if (targets.Count > 0) + { + percentPerItem /= targets.Count; + } + + foreach (var target in targets) + { + cancellationToken.ThrowIfCancellationRequested(); + + var currentPercent = startingPercent; + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(pct => + { + var totalProgress = pct * percentPerItem; + totalProgress += currentPercent; + progress.Report(totalProgress); + }); + + var dataProvider = _syncManager.GetDataProvider(target.Item1, target.Item2); + + await new MediaSync(_logger, _syncManager, _appHost, _fileSystem, _config, _cryptographyProvider) + .Sync(target.Item1, dataProvider, target.Item2, innerProgress, cancellationToken) + .ConfigureAwait(false); + + numComplete++; + startingPercent = numComplete; + startingPercent /= targets.Count; + startingPercent *= 100; + progress.Report(startingPercent); + } + } + } +} diff --git a/Emby.Server.Implementations/Sync/ServerSyncScheduledTask.cs b/Emby.Server.Implementations/Sync/ServerSyncScheduledTask.cs new file mode 100644 index 000000000..17171633e --- /dev/null +++ b/Emby.Server.Implementations/Sync/ServerSyncScheduledTask.cs @@ -0,0 +1,95 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.Sync +{ + class ServerSyncScheduledTask : IScheduledTask, IConfigurableScheduledTask + { + private readonly ISyncManager _syncManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; + private readonly IConfigurationManager _config; + private readonly ICryptographyProvider _cryptographyProvider; + + public ServerSyncScheduledTask(ISyncManager syncManager, ILogger logger, IFileSystem fileSystem, IServerApplicationHost appHost, IConfigurationManager config, ICryptographyProvider cryptographyProvider) + { + _syncManager = syncManager; + _logger = logger; + _fileSystem = fileSystem; + _appHost = appHost; + _config = config; + _cryptographyProvider = cryptographyProvider; + } + + public string Name + { + get { return "Cloud & Folder Sync"; } + } + + public string Description + { + get { return "Sync media to the cloud"; } + } + + public string Category + { + get + { + return "Sync"; + } + } + + public Task Execute(CancellationToken cancellationToken, IProgress progress) + { + return new MultiProviderSync((SyncManager)_syncManager, _appHost, _logger, _fileSystem, _config, _cryptographyProvider) + .Sync(ServerSyncProviders, progress, cancellationToken); + } + + public IEnumerable ServerSyncProviders + { + get { return ((SyncManager)_syncManager).ServerSyncProviders; } + } + + /// + /// Creates the triggers that define when the task will run + /// + public IEnumerable GetDefaultTriggers() + { + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks} + }; + } + public bool IsHidden + { + get { return !IsEnabled; } + } + + public bool IsEnabled + { + get { return ServerSyncProviders.Any(); } + } + + public bool IsLogged + { + get { return true; } + } + + public string Key + { + get { return "ServerSync"; } + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncConfig.cs b/Emby.Server.Implementations/Sync/SyncConfig.cs new file mode 100644 index 000000000..8a97326bd --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncConfig.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + ConfigurationType = typeof(SyncOptions), + Key = "sync" + } + }; + } + } + + public static class SyncExtensions + { + public static SyncOptions GetSyncOptions(this IConfigurationManager config) + { + return config.GetConfiguration("sync"); + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncConvertScheduledTask.cs b/Emby.Server.Implementations/Sync/SyncConvertScheduledTask.cs new file mode 100644 index 000000000..8dafac7e1 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncConvertScheduledTask.cs @@ -0,0 +1,89 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncConvertScheduledTask : IScheduledTask + { + private readonly ILibraryManager _libraryManager; + private readonly ISyncRepository _syncRepo; + private readonly ISyncManager _syncManager; + private readonly ILogger _logger; + private readonly IUserManager _userManager; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IMediaSourceManager _mediaSourceManager; + + public SyncConvertScheduledTask(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) + { + _libraryManager = libraryManager; + _syncRepo = syncRepo; + _syncManager = syncManager; + _logger = logger; + _userManager = userManager; + _tvSeriesManager = tvSeriesManager; + _mediaEncoder = mediaEncoder; + _subtitleEncoder = subtitleEncoder; + _config = config; + _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; + } + + public string Name + { + get { return "Convert media"; } + } + + public string Description + { + get { return "Runs scheduled sync jobs"; } + } + + public string Category + { + get + { + return "Sync"; + } + } + + public Task Execute(CancellationToken cancellationToken, IProgress progress) + { + return new SyncJobProcessor(_libraryManager, _syncRepo, (SyncManager)_syncManager, _logger, _userManager, _tvSeriesManager, _mediaEncoder, _subtitleEncoder, _config, _fileSystem, _mediaSourceManager) + .Sync(progress, cancellationToken); + } + + /// + /// Creates the triggers that define when the task will run + /// + /// IEnumerable{BaseTaskTrigger}. + public IEnumerable GetDefaultTriggers() + { + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks} + }; + } + + public string Key + { + get { return "SyncPrepare"; } + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncHelper.cs b/Emby.Server.Implementations/Sync/SyncHelper.cs new file mode 100644 index 000000000..da475f003 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncHelper.cs @@ -0,0 +1,24 @@ +using System; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncHelper + { + public static int? AdjustBitrate(int? profileBitrate, string quality) + { + if (profileBitrate.HasValue) + { + if (string.Equals(quality, "medium", StringComparison.OrdinalIgnoreCase)) + { + profileBitrate = Math.Min(profileBitrate.Value, 4000000); + } + else if (string.Equals(quality, "low", StringComparison.OrdinalIgnoreCase)) + { + profileBitrate = Math.Min(profileBitrate.Value, 1500000); + } + } + + return profileBitrate; + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncJobOptions.cs b/Emby.Server.Implementations/Sync/SyncJobOptions.cs new file mode 100644 index 000000000..8e4d8e2ed --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncJobOptions.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Model.Dlna; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncJobOptions + { + /// + /// Gets or sets the conversion options. + /// + /// The conversion options. + public DeviceProfile DeviceProfile { get; set; } + /// + /// Gets or sets a value indicating whether this instance is converting. + /// + /// true if this instance is converting; otherwise, false. + public bool IsConverting { get; set; } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncJobProcessor.cs b/Emby.Server.Implementations/Sync/SyncJobProcessor.cs new file mode 100644 index 000000000..415757609 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncJobProcessor.cs @@ -0,0 +1,988 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Session; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncJobProcessor + { + private readonly ILibraryManager _libraryManager; + private readonly ISyncRepository _syncRepo; + private readonly SyncManager _syncManager; + private readonly ILogger _logger; + private readonly IUserManager _userManager; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly ISubtitleEncoder _subtitleEncoder; + private readonly IConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IMediaSourceManager _mediaSourceManager; + + public SyncJobProcessor(ILibraryManager libraryManager, ISyncRepository syncRepo, SyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) + { + _libraryManager = libraryManager; + _syncRepo = syncRepo; + _syncManager = syncManager; + _logger = logger; + _userManager = userManager; + _tvSeriesManager = tvSeriesManager; + _mediaEncoder = mediaEncoder; + _subtitleEncoder = subtitleEncoder; + _config = config; + _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; + } + + public async Task EnsureJobItems(SyncJob job) + { + var user = _userManager.GetUserById(job.UserId); + + if (user == null) + { + throw new InvalidOperationException("Cannot proceed with sync because user no longer exists."); + } + + var items = (await GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false)) + .ToList(); + + var jobItems = _syncManager.GetJobItems(new SyncJobItemQuery + { + JobId = job.Id, + AddMetadata = false + + }).Items.ToList(); + + foreach (var item in items) + { + // Respect ItemLimit, if set + if (job.ItemLimit.HasValue) + { + if (jobItems.Count(j => j.Status != SyncJobItemStatus.RemovedFromDevice && j.Status != SyncJobItemStatus.Failed) >= job.ItemLimit.Value) + { + break; + } + } + + var itemId = item.Id.ToString("N"); + + var jobItem = jobItems.FirstOrDefault(i => string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)); + + if (jobItem != null) + { + continue; + } + + var index = jobItems.Count == 0 ? + 0 : + jobItems.Select(i => i.JobItemIndex).Max() + 1; + + jobItem = new SyncJobItem + { + Id = Guid.NewGuid().ToString("N"), + ItemId = itemId, + ItemName = GetSyncJobItemName(item), + JobId = job.Id, + TargetId = job.TargetId, + DateCreated = DateTime.UtcNow, + JobItemIndex = index + }; + + await _syncRepo.Create(jobItem).ConfigureAwait(false); + _syncManager.OnSyncJobItemCreated(jobItem); + + jobItems.Add(jobItem); + } + + jobItems = jobItems + .OrderBy(i => i.DateCreated) + .ToList(); + + await UpdateJobStatus(job, jobItems).ConfigureAwait(false); + } + + private string GetSyncJobItemName(BaseItem item) + { + var name = item.Name; + var episode = item as Episode; + + if (episode != null) + { + if (episode.IndexNumber.HasValue) + { + name = "E" + episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture) + " - " + name; + } + + if (episode.ParentIndexNumber.HasValue) + { + name = "S" + episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture) + ", " + name; + } + } + + return name; + } + + public Task UpdateJobStatus(string id) + { + var job = _syncRepo.GetJob(id); + + if (job == null) + { + return Task.FromResult(true); + } + + var result = _syncManager.GetJobItems(new SyncJobItemQuery + { + JobId = job.Id, + AddMetadata = false + }); + + return UpdateJobStatus(job, result.Items.ToList()); + } + + private async Task UpdateJobStatus(SyncJob job, List jobItems) + { + job.ItemCount = jobItems.Count; + + double pct = 0; + + foreach (var item in jobItems) + { + if (item.Status == SyncJobItemStatus.Failed || item.Status == SyncJobItemStatus.Synced || item.Status == SyncJobItemStatus.RemovedFromDevice || item.Status == SyncJobItemStatus.Cancelled) + { + pct += 100; + } + else + { + pct += item.Progress ?? 0; + } + } + + if (job.ItemCount > 0) + { + pct /= job.ItemCount; + job.Progress = pct; + } + else + { + job.Progress = null; + } + + if (jobItems.Any(i => i.Status == SyncJobItemStatus.Transferring)) + { + job.Status = SyncJobStatus.Transferring; + } + else if (jobItems.Any(i => i.Status == SyncJobItemStatus.Converting)) + { + job.Status = SyncJobStatus.Converting; + } + else if (jobItems.All(i => i.Status == SyncJobItemStatus.Failed)) + { + job.Status = SyncJobStatus.Failed; + } + else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled)) + { + job.Status = SyncJobStatus.Cancelled; + } + else if (jobItems.All(i => i.Status == SyncJobItemStatus.ReadyToTransfer)) + { + job.Status = SyncJobStatus.ReadyToTransfer; + } + else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled || i.Status == SyncJobItemStatus.Failed || i.Status == SyncJobItemStatus.Synced || i.Status == SyncJobItemStatus.RemovedFromDevice)) + { + if (jobItems.Any(i => i.Status == SyncJobItemStatus.Failed)) + { + job.Status = SyncJobStatus.CompletedWithError; + } + else + { + job.Status = SyncJobStatus.Completed; + } + } + else + { + job.Status = SyncJobStatus.Queued; + } + + await _syncRepo.Update(job).ConfigureAwait(false); + + _syncManager.OnSyncJobUpdated(job); + } + + public async Task> GetItemsForSync(SyncCategory? category, string parentId, IEnumerable itemIds, User user, bool unwatchedOnly) + { + var list = new List(); + + if (category.HasValue) + { + list = (await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false)).ToList(); + } + else + { + foreach (var itemId in itemIds) + { + var subList = await GetItemsForSync(itemId, user).ConfigureAwait(false); + list.AddRange(subList); + } + } + + IEnumerable items = list; + items = items.Where(_syncManager.SupportsSync); + + if (unwatchedOnly) + { + // Avoid implicitly captured closure + var currentUser = user; + + items = items.Where(i => + { + var video = i as Video; + + if (video != null) + { + return !video.IsPlayed(currentUser); + } + + return true; + }); + } + + return items.DistinctBy(i => i.Id); + } + + private async Task> GetItemsForSync(SyncCategory category, string parentId, User user) + { + var parent = string.IsNullOrWhiteSpace(parentId) + ? user.RootFolder + : (Folder)_libraryManager.GetItemById(parentId); + + InternalItemsQuery query; + + switch (category) + { + case SyncCategory.Latest: + query = new InternalItemsQuery + { + IsFolder = false, + SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }, + SortOrder = SortOrder.Descending, + Recursive = true + }; + break; + case SyncCategory.Resume: + query = new InternalItemsQuery + { + IsFolder = false, + SortBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }, + SortOrder = SortOrder.Descending, + Recursive = true, + IsResumable = true, + MediaTypes = new[] { MediaType.Video } + }; + break; + + case SyncCategory.NextUp: + return _tvSeriesManager.GetNextUp(new NextUpQuery + { + ParentId = parentId, + UserId = user.Id.ToString("N") + }).Items; + + default: + throw new ArgumentException("Unrecognized category: " + category); + } + + if (parent == null) + { + return new List(); + } + + query.User = user; + + var result = await parent.GetItems(query).ConfigureAwait(false); + return result.Items; + } + + private async Task> GetItemsForSync(string id, User user) + { + var item = _libraryManager.GetItemById(id); + + if (item == null) + { + return new List(); + } + + var itemByName = item as IItemByName; + if (itemByName != null) + { + return itemByName.GetTaggedItems(new InternalItemsQuery(user) + { + IsFolder = false, + Recursive = true + }).ToList(); + } + + if (item.IsFolder) + { + var folder = (Folder)item; + var itemsResult = await folder.GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false + + }).ConfigureAwait(false); + + var items = itemsResult.Items; + + if (!folder.IsPreSorted) + { + items = _libraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) + .ToArray(); + } + + return items.ToList(); + } + + return new List { item }; + } + + private async Task EnsureSyncJobItems(string targetId, CancellationToken cancellationToken) + { + var jobResult = _syncRepo.GetJobs(new SyncJobQuery + { + SyncNewContent = true, + TargetId = targetId + }); + + foreach (var job in jobResult.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (job.SyncNewContent) + { + await EnsureJobItems(job).ConfigureAwait(false); + } + } + } + + public async Task Sync(IProgress progress, CancellationToken cancellationToken) + { + await EnsureSyncJobItems(null, cancellationToken).ConfigureAwait(false); + + // Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow + await HandleDeletedSyncFiles(cancellationToken).ConfigureAwait(false); + + // If it already has a converting status then is must have been aborted during conversion + var result = _syncManager.GetJobItems(new SyncJobItemQuery + { + Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + AddMetadata = false + }); + + await SyncJobItems(result.Items, true, progress, cancellationToken).ConfigureAwait(false); + + CleanDeadSyncFiles(); + } + + private async Task HandleDeletedSyncFiles(CancellationToken cancellationToken) + { + // Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow + var result = _syncManager.GetJobItems(new SyncJobItemQuery + { + Statuses = new[] { SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Transferring }, + AddMetadata = false + }); + + foreach (var item in result.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(item.OutputPath) || !_fileSystem.FileExists(item.OutputPath)) + { + item.Status = SyncJobItemStatus.Queued; + await _syncManager.UpdateSyncJobItemInternal(item).ConfigureAwait(false); + await UpdateJobStatus(item.JobId).ConfigureAwait(false); + } + } + } + + private void CleanDeadSyncFiles() + { + // TODO + // Clean files in sync temp folder that are not linked to any sync jobs + } + + public async Task SyncJobItems(string targetId, bool enableConversion, IProgress progress, + CancellationToken cancellationToken) + { + await EnsureSyncJobItems(targetId, cancellationToken).ConfigureAwait(false); + + // If it already has a converting status then is must have been aborted during conversion + var result = _syncManager.GetJobItems(new SyncJobItemQuery + { + Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + TargetId = targetId, + AddMetadata = false + }); + + await SyncJobItems(result.Items, enableConversion, progress, cancellationToken).ConfigureAwait(false); + } + + public async Task SyncJobItems(SyncJobItem[] items, bool enableConversion, IProgress progress, CancellationToken cancellationToken) + { + if (items.Length > 0) + { + if (!SyncRegistrationInfo.Instance.IsRegistered) + { + _logger.Debug("Cancelling sync job processing. Please obtain a supporter membership."); + return; + } + } + + var numComplete = 0; + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + double percentPerItem = 1; + percentPerItem /= items.Length; + var startingPercent = numComplete * percentPerItem * 100; + + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(startingPercent + percentPerItem * p)); + + // Pull it fresh from the db just to make sure it wasn't deleted or cancelled while another item was converting + var jobItem = enableConversion ? _syncRepo.GetJobItem(item.Id) : item; + + if (jobItem != null) + { + if (jobItem.Status != SyncJobItemStatus.Cancelled) + { + await ProcessJobItem(jobItem, enableConversion, innerProgress, cancellationToken).ConfigureAwait(false); + } + + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= items.Length; + progress.Report(100 * percent); + } + } + + private async Task ProcessJobItem(SyncJobItem jobItem, bool enableConversion, IProgress progress, CancellationToken cancellationToken) + { + if (jobItem == null) + { + throw new ArgumentNullException("jobItem"); + } + + var item = _libraryManager.GetItemById(jobItem.ItemId); + if (item == null) + { + jobItem.Status = SyncJobItemStatus.Failed; + _logger.Error("Unable to locate library item for JobItem {0}, ItemId {1}", jobItem.Id, jobItem.ItemId); + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + return; + } + + jobItem.Progress = 0; + + var syncOptions = _config.GetSyncOptions(); + var job = _syncManager.GetJob(jobItem.JobId); + var user = _userManager.GetUserById(job.UserId); + if (user == null) + { + jobItem.Status = SyncJobItemStatus.Failed; + _logger.Error("User not found. Cannot complete the sync job."); + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + return; + } + + // See if there's already another active job item for the same target + var existingJobItems = _syncManager.GetJobItems(new SyncJobItemQuery + { + AddMetadata = false, + ItemId = jobItem.ItemId, + TargetId = jobItem.TargetId, + Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring } + }); + + var duplicateJobItems = existingJobItems.Items + .Where(i => !string.Equals(i.Id, jobItem.Id, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (duplicateJobItems.Count > 0) + { + var syncProvider = _syncManager.GetSyncProvider(jobItem) as IHasDuplicateCheck; + + if (!duplicateJobItems.Any(i => AllowDuplicateJobItem(syncProvider, i, jobItem))) + { + _logger.Debug("Cancelling sync job item because there is already another active job for the same target."); + jobItem.Status = SyncJobItemStatus.Cancelled; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + return; + } + } + + var video = item as Video; + if (video != null) + { + await Sync(jobItem, video, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); + } + + else if (item is Audio) + { + await Sync(jobItem, (Audio)item, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); + } + + else if (item is Photo) + { + await Sync(jobItem, (Photo)item, cancellationToken).ConfigureAwait(false); + } + + else + { + await SyncGeneric(jobItem, item, cancellationToken).ConfigureAwait(false); + } + } + + private bool AllowDuplicateJobItem(IHasDuplicateCheck provider, SyncJobItem original, SyncJobItem duplicate) + { + if (provider != null) + { + return provider.AllowDuplicateJobItem(original, duplicate); + } + + return true; + } + + private async Task Sync(SyncJobItem jobItem, Video item, User user, bool enableConversion, SyncOptions syncOptions, IProgress progress, CancellationToken cancellationToken) + { + var job = _syncManager.GetJob(jobItem.JobId); + var jobOptions = _syncManager.GetVideoOptions(jobItem, job); + var conversionOptions = new VideoOptions + { + Profile = jobOptions.DeviceProfile + }; + + conversionOptions.DeviceId = jobItem.TargetId; + conversionOptions.Context = EncodingContext.Static; + conversionOptions.ItemId = item.Id.ToString("N"); + conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); + + var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(conversionOptions); + var mediaSource = streamInfo.MediaSource; + + // No sense creating external subs if we're already burning one into the video + var externalSubs = streamInfo.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode ? + new List() : + streamInfo.GetExternalSubtitles(false, true, null, null); + + // Mark as requiring conversion if transcoding the video, or if any subtitles need to be extracted + var requiresVideoTranscoding = streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting; + var requiresConversion = requiresVideoTranscoding || externalSubs.Any(i => RequiresExtraction(i, mediaSource)); + + if (requiresConversion && !enableConversion) + { + return; + } + + jobItem.MediaSourceId = streamInfo.MediaSourceId; + jobItem.TemporaryPath = GetTemporaryPath(jobItem); + + if (requiresConversion) + { + jobItem.Status = SyncJobItemStatus.Converting; + } + + if (requiresVideoTranscoding) + { + // Save the job item now since conversion could take a while + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + + try + { + var lastJobUpdate = DateTime.MinValue; + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(async pct => + { + progress.Report(pct); + + if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds) + { + jobItem.Progress = pct / 2; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + }); + + jobItem.OutputPath = await _mediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, conversionOptions.Profile) + { + OutputDirectory = jobItem.TemporaryPath, + CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit, + ReadInputAtNativeFramerate = !syncOptions.EnableFullSpeedTranscoding + + }, innerProgress, cancellationToken); + + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + _syncManager.OnConversionComplete(jobItem); + } + catch (OperationCanceledException) + { + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + } + catch (Exception ex) + { + jobItem.Status = SyncJobItemStatus.Failed; + _logger.ErrorException("Error during sync transcoding", ex); + } + + if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued) + { + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + return; + } + + jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, true).ConfigureAwait(false); + } + else + { + if (mediaSource.Protocol == MediaProtocol.File) + { + jobItem.OutputPath = mediaSource.Path; + } + else if (mediaSource.Protocol == MediaProtocol.Http) + { + jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); + } + + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + jobItem.MediaSource = mediaSource; + } + + jobItem.MediaSource.SupportsTranscoding = false; + + if (externalSubs.Count > 0) + { + // Save the job item now since conversion could take a while + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + await ConvertSubtitles(jobItem, externalSubs, streamInfo, cancellationToken).ConfigureAwait(false); + } + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + + private bool RequiresExtraction(SubtitleStreamInfo stream, MediaSourceInfo mediaSource) + { + var originalStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Subtitle && i.Index == stream.Index); + + return originalStream != null && !originalStream.IsExternal; + } + + private async Task ConvertSubtitles(SyncJobItem jobItem, + IEnumerable subtitles, + StreamInfo streamInfo, + CancellationToken cancellationToken) + { + var files = new List(); + + var mediaStreams = jobItem.MediaSource.MediaStreams + .Where(i => i.Type != MediaStreamType.Subtitle || !i.IsExternal) + .ToList(); + + var startingIndex = mediaStreams.Count == 0 ? + 0 : + mediaStreams.Select(i => i.Index).Max() + 1; + + foreach (var subtitle in subtitles) + { + var fileInfo = await ConvertSubtitles(jobItem.TemporaryPath, streamInfo, subtitle, cancellationToken).ConfigureAwait(false); + + // Reset this to a value that will be based on the output media + fileInfo.Index = startingIndex; + files.Add(fileInfo); + + mediaStreams.Add(new MediaStream + { + Index = startingIndex, + Codec = subtitle.Format, + IsForced = subtitle.IsForced, + IsExternal = true, + Language = subtitle.Language, + Path = fileInfo.Path, + SupportsExternalStream = true, + Type = MediaStreamType.Subtitle + }); + + startingIndex++; + } + + jobItem.AdditionalFiles.AddRange(files); + + jobItem.MediaSource.MediaStreams = mediaStreams; + } + + private async Task ConvertSubtitles(string temporaryPath, StreamInfo streamInfo, SubtitleStreamInfo subtitleStreamInfo, CancellationToken cancellationToken) + { + var subtitleStreamIndex = subtitleStreamInfo.Index; + + var filename = Guid.NewGuid() + "." + subtitleStreamInfo.Format.ToLower(); + + var path = Path.Combine(temporaryPath, filename); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, false, cancellationToken).ConfigureAwait(false)) + { + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + { + await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + + return new ItemFileInfo + { + Name = Path.GetFileName(path), + Path = path, + Type = ItemFileType.Subtitles, + Index = subtitleStreamIndex + }; + } + + private const int DatabaseProgressUpdateIntervalSeconds = 2; + + private async Task Sync(SyncJobItem jobItem, Audio item, User user, bool enableConversion, SyncOptions syncOptions, IProgress progress, CancellationToken cancellationToken) + { + var job = _syncManager.GetJob(jobItem.JobId); + var jobOptions = _syncManager.GetAudioOptions(jobItem, job); + var conversionOptions = new AudioOptions + { + Profile = jobOptions.DeviceProfile + }; + + conversionOptions.DeviceId = jobItem.TargetId; + conversionOptions.Context = EncodingContext.Static; + conversionOptions.ItemId = item.Id.ToString("N"); + conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); + + var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(conversionOptions); + var mediaSource = streamInfo.MediaSource; + + jobItem.MediaSourceId = streamInfo.MediaSourceId; + jobItem.TemporaryPath = GetTemporaryPath(jobItem); + + if (streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting) + { + if (!enableConversion) + { + return; + } + + jobItem.Status = SyncJobItemStatus.Converting; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + + try + { + var lastJobUpdate = DateTime.MinValue; + var innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(async pct => + { + progress.Report(pct); + + if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds) + { + jobItem.Progress = pct / 2; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + }); + + jobItem.OutputPath = await _mediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, conversionOptions.Profile) + { + OutputDirectory = jobItem.TemporaryPath, + CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit + + }, innerProgress, cancellationToken); + + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + _syncManager.OnConversionComplete(jobItem); + } + catch (OperationCanceledException) + { + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + } + catch (Exception ex) + { + jobItem.Status = SyncJobItemStatus.Failed; + _logger.ErrorException("Error during sync transcoding", ex); + } + + if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued) + { + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + return; + } + + jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, false).ConfigureAwait(false); + } + else + { + if (mediaSource.Protocol == MediaProtocol.File) + { + jobItem.OutputPath = mediaSource.Path; + } + else if (mediaSource.Protocol == MediaProtocol.Http) + { + jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); + } + + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + jobItem.MediaSource = mediaSource; + } + + jobItem.MediaSource.SupportsTranscoding = false; + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + + private async Task Sync(SyncJobItem jobItem, Photo item, CancellationToken cancellationToken) + { + jobItem.OutputPath = item.Path; + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + + private async Task SyncGeneric(SyncJobItem jobItem, BaseItem item, CancellationToken cancellationToken) + { + jobItem.OutputPath = item.Path; + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + + private async Task DownloadFile(SyncJobItem jobItem, MediaSourceInfo mediaSource, CancellationToken cancellationToken) + { + // TODO: Download + return mediaSource.Path; + } + + public string GetTemporaryPath(SyncJob job) + { + return GetTemporaryPath(job.Id); + } + + public string GetTemporaryPath(string jobId) + { + var basePath = _config.GetSyncOptions().TemporaryPath; + + if (string.IsNullOrWhiteSpace(basePath)) + { + basePath = Path.Combine(_config.CommonApplicationPaths.ProgramDataPath, "sync"); + } + + return Path.Combine(basePath, jobId); + } + + public string GetTemporaryPath(SyncJobItem jobItem) + { + return Path.Combine(GetTemporaryPath(jobItem.JobId), jobItem.Id); + } + + private async Task GetEncodedMediaSource(string path, User user, bool isVideo) + { + var item = _libraryManager.ResolvePath(_fileSystem.GetFileSystemInfo(path)); + + await item.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); + + var hasMediaSources = item as IHasMediaSources; + + var mediaSources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false).ToList(); + + var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) + ? new string[] { } + : new[] { user.Configuration.AudioLanguagePreference }; + + var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) + ? new List() : new List { user.Configuration.SubtitleLanguagePreference }; + + foreach (var source in mediaSources) + { + if (isVideo) + { + source.DefaultAudioStreamIndex = + MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); + + var defaultAudioIndex = source.DefaultAudioStreamIndex; + var audioLangage = defaultAudioIndex == null + ? null + : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); + + source.DefaultAudioStreamIndex = + MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, preferredSubs, user.Configuration.SubtitleMode, audioLangage); + } + else + { + var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + if (audio != null) + { + source.DefaultAudioStreamIndex = audio.Index; + } + + } + } + + return mediaSources.FirstOrDefault(); + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncManager.cs b/Emby.Server.Implementations/Sync/SyncManager.cs new file mode 100644 index 000000000..d06ed49fd --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncManager.cs @@ -0,0 +1,1362 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Controller.TV; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; +using MediaBrowser.Model.Users; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncManager : ISyncManager + { + private readonly ILibraryManager _libraryManager; + private readonly ISyncRepository _repo; + private readonly IImageProcessor _imageProcessor; + private readonly ILogger _logger; + private readonly IUserManager _userManager; + private readonly Func _dtoService; + private readonly IServerApplicationHost _appHost; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly Func _mediaEncoder; + private readonly IFileSystem _fileSystem; + private readonly Func _subtitleEncoder; + private readonly IConfigurationManager _config; + private readonly IUserDataManager _userDataManager; + private readonly Func _mediaSourceManager; + private readonly IJsonSerializer _json; + private readonly ITaskManager _taskManager; + private readonly IMemoryStreamProvider _memoryStreamProvider; + + private ISyncProvider[] _providers = { }; + + public event EventHandler> SyncJobCreated; + public event EventHandler> SyncJobCancelled; + public event EventHandler> SyncJobUpdated; + public event EventHandler> SyncJobItemUpdated; + public event EventHandler> SyncJobItemCreated; + + public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func mediaEncoder, IFileSystem fileSystem, Func subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func mediaSourceManager, IJsonSerializer json, ITaskManager taskManager, IMemoryStreamProvider memoryStreamProvider) + { + _libraryManager = libraryManager; + _repo = repo; + _imageProcessor = imageProcessor; + _logger = logger; + _userManager = userManager; + _dtoService = dtoService; + _appHost = appHost; + _tvSeriesManager = tvSeriesManager; + _mediaEncoder = mediaEncoder; + _fileSystem = fileSystem; + _subtitleEncoder = subtitleEncoder; + _config = config; + _userDataManager = userDataManager; + _mediaSourceManager = mediaSourceManager; + _json = json; + _taskManager = taskManager; + _memoryStreamProvider = memoryStreamProvider; + } + + public void AddParts(IEnumerable providers) + { + _providers = providers.ToArray(); + } + + public IEnumerable ServerSyncProviders + { + get { return _providers.OfType(); } + } + + private readonly ConcurrentDictionary _dataProviders = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + public ISyncDataProvider GetDataProvider(IServerSyncProvider provider, SyncTarget target) + { + return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths, _memoryStreamProvider)); + } + + public async Task CreateJob(SyncJobRequest request) + { + var processor = GetSyncJobProcessor(); + + var user = _userManager.GetUserById(request.UserId); + + var items = (await processor + .GetItemsForSync(request.Category, request.ParentId, request.ItemIds, user, request.UnwatchedOnly).ConfigureAwait(false)) + .ToList(); + + if (items.Any(i => !SupportsSync(i))) + { + throw new ArgumentException("Item does not support sync."); + } + + if (string.IsNullOrWhiteSpace(request.Name)) + { + if (request.ItemIds.Count == 1) + { + request.Name = GetDefaultName(_libraryManager.GetItemById(request.ItemIds[0])); + } + } + + if (string.IsNullOrWhiteSpace(request.Name)) + { + request.Name = DateTime.Now.ToString("f1", CultureInfo.CurrentCulture); + } + + var target = GetSyncTargets(request.UserId) + .FirstOrDefault(i => string.Equals(request.TargetId, i.Id)); + + if (target == null) + { + throw new ArgumentException("Sync target not found."); + } + + var jobId = Guid.NewGuid().ToString("N"); + + if (string.IsNullOrWhiteSpace(request.Quality)) + { + request.Quality = GetQualityOptions(request.TargetId) + .Where(i => i.IsDefault) + .Select(i => i.Id) + .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); + } + + var job = new SyncJob + { + Id = jobId, + Name = request.Name, + TargetId = target.Id, + UserId = request.UserId, + UnwatchedOnly = request.UnwatchedOnly, + ItemLimit = request.ItemLimit, + RequestedItemIds = request.ItemIds ?? new List(), + DateCreated = DateTime.UtcNow, + DateLastModified = DateTime.UtcNow, + SyncNewContent = request.SyncNewContent, + ItemCount = items.Count, + Category = request.Category, + ParentId = request.ParentId, + Quality = request.Quality, + Profile = request.Profile, + Bitrate = request.Bitrate + }; + + if (!request.Category.HasValue && request.ItemIds != null) + { + var requestedItems = request.ItemIds + .Select(_libraryManager.GetItemById) + .Where(i => i != null); + + // It's just a static list + if (!requestedItems.Any(i => i.IsFolder || i is IItemByName)) + { + job.SyncNewContent = false; + } + } + + await _repo.Create(job).ConfigureAwait(false); + + await processor.EnsureJobItems(job).ConfigureAwait(false); + + // If it already has a converting status then is must have been aborted during conversion + var jobItemsResult = GetJobItems(new SyncJobItemQuery + { + Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + JobId = jobId, + AddMetadata = false + }); + + await processor.SyncJobItems(jobItemsResult.Items, false, new Progress(), CancellationToken.None) + .ConfigureAwait(false); + + jobItemsResult = GetJobItems(new SyncJobItemQuery + { + Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + JobId = jobId, + AddMetadata = false + }); + + var returnResult = new SyncJobCreationResult + { + Job = GetJob(jobId), + JobItems = jobItemsResult.Items.ToList() + }; + + if (SyncJobCreated != null) + { + EventHelper.FireEventIfNotNull(SyncJobCreated, this, new GenericEventArgs + { + Argument = returnResult + + }, _logger); + } + + if (returnResult.JobItems.Any(i => i.Status == SyncJobItemStatus.Queued || i.Status == SyncJobItemStatus.Converting)) + { + _taskManager.QueueScheduledTask(); + } + + return returnResult; + } + + public async Task UpdateJob(SyncJob job) + { + // Get fresh from the db and only update the fields that are supported to be changed. + var instance = _repo.GetJob(job.Id); + + instance.Name = job.Name; + instance.Quality = job.Quality; + instance.Profile = job.Profile; + instance.UnwatchedOnly = job.UnwatchedOnly; + instance.SyncNewContent = job.SyncNewContent; + instance.ItemLimit = job.ItemLimit; + + await _repo.Update(instance).ConfigureAwait(false); + + OnSyncJobUpdated(instance); + } + + internal void OnSyncJobUpdated(SyncJob job) + { + if (SyncJobUpdated != null) + { + EventHelper.FireEventIfNotNull(SyncJobUpdated, this, new GenericEventArgs + { + Argument = job + + }, _logger); + } + } + + internal async Task UpdateSyncJobItemInternal(SyncJobItem jobItem) + { + await _repo.Update(jobItem).ConfigureAwait(false); + + if (SyncJobUpdated != null) + { + EventHelper.FireEventIfNotNull(SyncJobItemUpdated, this, new GenericEventArgs + { + Argument = jobItem + + }, _logger); + } + } + + internal void OnSyncJobItemCreated(SyncJobItem job) + { + if (SyncJobUpdated != null) + { + EventHelper.FireEventIfNotNull(SyncJobItemCreated, this, new GenericEventArgs + { + Argument = job + + }, _logger); + } + } + + public async Task> GetJobs(SyncJobQuery query) + { + var result = _repo.GetJobs(query); + + foreach (var item in result.Items) + { + await FillMetadata(item).ConfigureAwait(false); + } + + return result; + } + + private async Task FillMetadata(SyncJob job) + { + var user = _userManager.GetUserById(job.UserId); + + if (user == null) + { + return; + } + + var target = GetSyncTargets(job.UserId) + .FirstOrDefault(i => string.Equals(i.Id, job.TargetId, StringComparison.OrdinalIgnoreCase)); + + if (target != null) + { + job.TargetName = target.Name; + } + + var item = job.RequestedItemIds + .Select(_libraryManager.GetItemById) + .FirstOrDefault(i => i != null); + + if (item == null) + { + var processor = GetSyncJobProcessor(); + + item = (await processor + .GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false)) + .FirstOrDefault(); + } + + if (item != null) + { + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + job.ParentName = hasSeries.SeriesName; + } + + var hasAlbumArtist = item as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + job.ParentName = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + } + + var primaryImage = item.GetImageInfo(ImageType.Primary, 0); + var itemWithImage = item; + + if (primaryImage == null) + { + var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); + + if (parentWithImage != null) + { + itemWithImage = parentWithImage; + primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0); + } + } + + if (primaryImage != null) + { + try + { + job.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary); + job.PrimaryImageItemId = itemWithImage.Id.ToString("N"); + + } + catch (Exception ex) + { + _logger.ErrorException("Error getting image info", ex); + } + } + } + } + + private void FillMetadata(SyncJobItem jobItem) + { + var item = _libraryManager.GetItemById(jobItem.ItemId); + + if (item == null) + { + return; + } + + var primaryImage = item.GetImageInfo(ImageType.Primary, 0); + var itemWithImage = item; + + if (primaryImage == null) + { + var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); + + if (parentWithImage != null) + { + itemWithImage = parentWithImage; + primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0); + } + } + + if (primaryImage != null) + { + try + { + jobItem.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary); + jobItem.PrimaryImageItemId = itemWithImage.Id.ToString("N"); + + } + catch (Exception ex) + { + _logger.ErrorException("Error getting image info", ex); + } + } + } + + public async Task CancelJob(string id) + { + var job = GetJob(id); + + if (job == null) + { + throw new ArgumentException("Job not found."); + } + + await _repo.DeleteJob(id).ConfigureAwait(false); + + var path = GetSyncJobProcessor().GetTemporaryPath(id); + + try + { + _fileSystem.DeleteDirectory(path, true); + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting directory {0}", ex, path); + } + + if (SyncJobCancelled != null) + { + EventHelper.FireEventIfNotNull(SyncJobCancelled, this, new GenericEventArgs + { + Argument = job + + }, _logger); + } + } + + public SyncJob GetJob(string id) + { + return _repo.GetJob(id); + } + + public IEnumerable GetSyncTargets(string userId) + { + return _providers + .SelectMany(i => GetSyncTargets(i, userId)) + .OrderBy(i => i.Name); + } + + private IEnumerable GetSyncTargets(ISyncProvider provider) + { + return provider.GetAllSyncTargets().Select(i => new SyncTarget + { + Name = i.Name, + Id = GetSyncTargetId(provider, i) + }); + } + + private IEnumerable GetSyncTargets(ISyncProvider provider, string userId) + { + return provider.GetSyncTargets(userId).Select(i => new SyncTarget + { + Name = i.Name, + Id = GetSyncTargetId(provider, i) + }); + } + + private string GetSyncTargetId(ISyncProvider provider, SyncTarget target) + { + var hasUniqueId = provider as IHasUniqueTargetIds; + + if (hasUniqueId != null) + { + return target.Id; + } + + return target.Id; + //var providerId = GetSyncProviderId(provider); + //return (providerId + "-" + target.Id).GetMD5().ToString("N"); + } + + private string GetSyncProviderId(ISyncProvider provider) + { + return provider.GetType().Name.GetMD5().ToString("N"); + } + + public bool SupportsSync(BaseItem item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + if (item is Playlist) + { + return true; + } + + if (item is Person) + { + return false; + } + + if (item is Year) + { + return false; + } + + if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) || + string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) || + string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase) || + string.Equals(item.MediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase) || + string.Equals(item.MediaType, MediaType.Book, StringComparison.OrdinalIgnoreCase)) + { + if (item.LocationType == LocationType.Virtual) + { + return false; + } + + var video = item as Video; + if (video != null) + { + if (video.IsPlaceHolder) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + } + + if (item.SourceType != SourceType.Library) + { + return false; + } + + return true; + } + + if (item.SourceType == SourceType.Channel) + { + return BaseItem.ChannelManager.SupportsSync(item.ChannelId); + } + + return item.LocationType == LocationType.FileSystem || item is Season; + } + + private string GetDefaultName(BaseItem item) + { + return item.Name; + } + + public async Task ReportSyncJobItemTransferred(string id) + { + var jobItem = _repo.GetJobItem(id); + + jobItem.Status = SyncJobItemStatus.Synced; + jobItem.Progress = 100; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(jobItem.TemporaryPath)) + { + try + { + _fileSystem.DeleteDirectory(jobItem.TemporaryPath, true); + } + catch (IOException) + { + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting temporary job file: {0}", ex, jobItem.OutputPath); + } + } + } + + private SyncJobProcessor GetSyncJobProcessor() + { + return new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, _mediaEncoder(), _subtitleEncoder(), _config, _fileSystem, _mediaSourceManager()); + } + + public SyncJobItem GetJobItem(string id) + { + return _repo.GetJobItem(id); + } + + public QueryResult GetJobItems(SyncJobItemQuery query) + { + var result = _repo.GetJobItems(query); + + if (query.AddMetadata) + { + foreach (var item in result.Items) + { + FillMetadata(item); + } + } + + return result; + } + + private SyncedItem GetJobItemInfo(SyncJobItem jobItem) + { + var job = _repo.GetJob(jobItem.JobId); + + if (job == null) + { + _logger.Error("GetJobItemInfo job id {0} no longer exists", jobItem.JobId); + return null; + } + + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + + if (libraryItem == null) + { + _logger.Error("GetJobItemInfo library item with id {0} no longer exists", jobItem.ItemId); + return null; + } + + var syncedItem = new SyncedItem + { + SyncJobId = jobItem.JobId, + SyncJobItemId = jobItem.Id, + ServerId = _appHost.SystemId, + UserId = job.UserId, + SyncJobName = job.Name, + SyncJobDateCreated = job.DateCreated, + AdditionalFiles = jobItem.AdditionalFiles.Select(i => new ItemFileInfo + { + ImageType = i.ImageType, + Name = i.Name, + Type = i.Type, + Index = i.Index + + }).ToList() + }; + + var dtoOptions = new DtoOptions(); + + // Remove some bloat + dtoOptions.Fields.Remove(ItemFields.MediaStreams); + dtoOptions.Fields.Remove(ItemFields.IndexOptions); + dtoOptions.Fields.Remove(ItemFields.MediaSourceCount); + dtoOptions.Fields.Remove(ItemFields.Path); + dtoOptions.Fields.Remove(ItemFields.SeriesGenres); + dtoOptions.Fields.Remove(ItemFields.Settings); + dtoOptions.Fields.Remove(ItemFields.SyncInfo); + dtoOptions.Fields.Remove(ItemFields.BasicSyncInfo); + + syncedItem.Item = _dtoService().GetBaseItemDto(libraryItem, dtoOptions); + + var mediaSource = jobItem.MediaSource; + + syncedItem.Item.MediaSources = new List(); + + syncedItem.OriginalFileName = Path.GetFileName(libraryItem.Path); + if (string.IsNullOrWhiteSpace(syncedItem.OriginalFileName)) + { + syncedItem.OriginalFileName = Path.GetFileName(mediaSource.Path); + } + + // This will be null for items that are not audio/video + if (mediaSource != null) + { + syncedItem.OriginalFileName = Path.ChangeExtension(syncedItem.OriginalFileName, Path.GetExtension(mediaSource.Path)); + syncedItem.Item.MediaSources.Add(mediaSource); + } + if (string.IsNullOrWhiteSpace(syncedItem.OriginalFileName)) + { + syncedItem.OriginalFileName = libraryItem.Name; + } + + return syncedItem; + } + + public Task ReportOfflineAction(UserAction action) + { + switch (action.Type) + { + case UserActionType.PlayedItem: + return ReportOfflinePlayedItem(action); + default: + throw new ArgumentException("Unexpected action type"); + } + } + + private Task ReportOfflinePlayedItem(UserAction action) + { + var item = _libraryManager.GetItemById(action.ItemId); + var userData = _userDataManager.GetUserData(action.UserId, item); + + userData.LastPlayedDate = action.Date; + _userDataManager.UpdatePlayState(item, userData, action.PositionTicks); + + return _userDataManager.SaveUserData(new Guid(action.UserId), item, userData, UserDataSaveReason.Import, CancellationToken.None); + } + + public async Task> GetReadySyncItems(string targetId) + { + var processor = GetSyncJobProcessor(); + + await processor.SyncJobItems(targetId, false, new Progress(), CancellationToken.None).ConfigureAwait(false); + + var jobItemResult = GetJobItems(new SyncJobItemQuery + { + TargetId = targetId, + Statuses = new[] + { + SyncJobItemStatus.ReadyToTransfer, + SyncJobItemStatus.Transferring + } + }); + + var readyItems = jobItemResult.Items + .Select(GetJobItemInfo) + .Where(i => i != null) + .ToList(); + + _logger.Debug("Returning {0} ready sync items for targetId {1}", readyItems.Count, targetId); + + return readyItems; + } + + public async Task SyncData(SyncDataRequest request) + { + if (request.SyncJobItemIds != null) + { + return await SyncDataUsingSyncJobItemIds(request).ConfigureAwait(false); + } + + var jobItemResult = GetJobItems(new SyncJobItemQuery + { + TargetId = request.TargetId, + Statuses = new[] { SyncJobItemStatus.Synced } + }); + + var response = new SyncDataResponse(); + + foreach (var jobItem in jobItemResult.Items) + { + var requiresSaving = false; + var removeFromDevice = false; + + if (request.LocalItemIds.Contains(jobItem.ItemId, StringComparer.OrdinalIgnoreCase)) + { + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + + var job = _repo.GetJob(jobItem.JobId); + var user = _userManager.GetUserById(job.UserId); + + if (jobItem.IsMarkedForRemoval) + { + // Tell the device to remove it since it has been marked for removal + _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.ItemId); + removeFromDevice = true; + } + else if (user == null) + { + // Tell the device to remove it since the user is gone now + _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.ItemId); + removeFromDevice = true; + } + else if (!IsLibraryItemAvailable(libraryItem)) + { + // Tell the device to remove it since it's no longer available + _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.ItemId); + removeFromDevice = true; + } + else if (job.UnwatchedOnly) + { + if (libraryItem is Video && libraryItem.IsPlayed(user)) + { + // Tell the device to remove it since it has been played + _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.ItemId); + removeFromDevice = true; + } + } + else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) + { + _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + requiresSaving = true; + } + } + else + { + // Content is no longer on the device + if (jobItem.IsMarkedForRemoval) + { + jobItem.Status = SyncJobItemStatus.RemovedFromDevice; + } + else + { + _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.ItemId); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + } + requiresSaving = true; + } + + if (removeFromDevice) + { + response.ItemIdsToRemove.Add(jobItem.ItemId); + jobItem.IsMarkedForRemoval = true; + requiresSaving = true; + } + + if (requiresSaving) + { + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + } + + // Now check each item that's on the device + foreach (var itemId in request.LocalItemIds) + { + // See if it's already marked for removal + if (response.ItemIdsToRemove.Contains(itemId, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + // If there isn't a sync job for this item, mark it for removal + if (!jobItemResult.Items.Any(i => string.Equals(itemId, i.ItemId, StringComparison.OrdinalIgnoreCase))) + { + response.ItemIdsToRemove.Add(itemId); + } + } + + response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + var itemsOnDevice = request.LocalItemIds + .Except(response.ItemIdsToRemove) + .ToList(); + + SetUserAccess(request, response, itemsOnDevice); + + return response; + } + + private async Task SyncDataUsingSyncJobItemIds(SyncDataRequest request) + { + var jobItemResult = GetJobItems(new SyncJobItemQuery + { + TargetId = request.TargetId, + Statuses = new[] { SyncJobItemStatus.Synced } + }); + + var response = new SyncDataResponse(); + + foreach (var jobItem in jobItemResult.Items) + { + var requiresSaving = false; + var removeFromDevice = false; + + if (request.SyncJobItemIds.Contains(jobItem.Id, StringComparer.OrdinalIgnoreCase)) + { + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + + var job = _repo.GetJob(jobItem.JobId); + var user = _userManager.GetUserById(job.UserId); + + if (jobItem.IsMarkedForRemoval) + { + // Tell the device to remove it since it has been marked for removal + _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.Id); + removeFromDevice = true; + } + else if (user == null) + { + // Tell the device to remove it since the user is gone now + _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.Id); + removeFromDevice = true; + } + else if (!IsLibraryItemAvailable(libraryItem)) + { + // Tell the device to remove it since it's no longer available + _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.Id); + removeFromDevice = true; + } + else if (job.UnwatchedOnly) + { + if (libraryItem is Video && libraryItem.IsPlayed(user)) + { + // Tell the device to remove it since it has been played + _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.Id); + removeFromDevice = true; + } + } + else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) + { + _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + requiresSaving = true; + } + } + else + { + // Content is no longer on the device + if (jobItem.IsMarkedForRemoval) + { + jobItem.Status = SyncJobItemStatus.RemovedFromDevice; + } + else + { + _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.Id); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + } + requiresSaving = true; + } + + if (removeFromDevice) + { + response.ItemIdsToRemove.Add(jobItem.Id); + jobItem.IsMarkedForRemoval = true; + requiresSaving = true; + } + + if (requiresSaving) + { + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + } + } + + // Now check each item that's on the device + foreach (var syncJobItemId in request.SyncJobItemIds) + { + // See if it's already marked for removal + if (response.ItemIdsToRemove.Contains(syncJobItemId, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + // If there isn't a sync job for this item, mark it for removal + if (!jobItemResult.Items.Any(i => string.Equals(syncJobItemId, i.Id, StringComparison.OrdinalIgnoreCase))) + { + response.ItemIdsToRemove.Add(syncJobItemId); + } + } + + response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + return response; + } + + private void SetUserAccess(SyncDataRequest request, SyncDataResponse response, List itemIds) + { + var users = request.OfflineUserIds + .Select(_userManager.GetUserById) + .Where(i => i != null) + .ToList(); + + foreach (var itemId in itemIds) + { + var item = _libraryManager.GetItemById(itemId); + + if (item != null) + { + response.ItemUserAccess[itemId] = users + .Where(i => IsUserVisible(item, i)) + .Select(i => i.Id.ToString("N")) + .OrderBy(i => i) + .ToList(); + } + } + } + + private bool IsUserVisible(BaseItem item, User user) + { + return item.IsVisibleStandalone(user); + } + + private bool IsLibraryItemAvailable(BaseItem item) + { + if (item == null) + { + return false; + } + + return true; + } + + public async Task ReEnableJobItem(string id) + { + var jobItem = _repo.GetJobItem(id); + + if (jobItem.Status != SyncJobItemStatus.Failed && jobItem.Status != SyncJobItemStatus.Cancelled) + { + throw new ArgumentException("Operation is not valid for this job item"); + } + + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + jobItem.IsMarkedForRemoval = false; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + public async Task CancelItems(string targetId, IEnumerable itemIds) + { + foreach (var item in itemIds) + { + var syncJobItemResult = GetJobItems(new SyncJobItemQuery + { + AddMetadata = false, + ItemId = item, + TargetId = targetId, + Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Converting, SyncJobItemStatus.Synced, SyncJobItemStatus.Failed } + }); + + foreach (var jobItem in syncJobItemResult.Items) + { + await CancelJobItem(jobItem.Id).ConfigureAwait(false); + } + } + } + + public async Task CancelJobItem(string id) + { + var jobItem = _repo.GetJobItem(id); + + if (jobItem.Status != SyncJobItemStatus.Queued && jobItem.Status != SyncJobItemStatus.ReadyToTransfer && jobItem.Status != SyncJobItemStatus.Converting && jobItem.Status != SyncJobItemStatus.Failed && jobItem.Status != SyncJobItemStatus.Synced && jobItem.Status != SyncJobItemStatus.Transferring) + { + throw new ArgumentException("Operation is not valid for this job item"); + } + + if (jobItem.Status != SyncJobItemStatus.Synced) + { + jobItem.Status = SyncJobItemStatus.Cancelled; + } + + jobItem.Progress = 0; + jobItem.IsMarkedForRemoval = true; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + + var path = processor.GetTemporaryPath(jobItem); + + try + { + _fileSystem.DeleteDirectory(path, true); + } + catch (IOException) + { + + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting directory {0}", ex, path); + } + + //var jobItemsResult = GetJobItems(new SyncJobItemQuery + //{ + // AddMetadata = false, + // JobId = jobItem.JobId, + // Limit = 0, + // Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Failed, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring } + //}); + + //if (jobItemsResult.TotalRecordCount == 0) + //{ + // await CancelJob(jobItem.JobId).ConfigureAwait(false); + //} + } + + public Task MarkJobItemForRemoval(string id) + { + return CancelJobItem(id); + } + + public async Task UnmarkJobItemForRemoval(string id) + { + var jobItem = _repo.GetJobItem(id); + + if (jobItem.Status != SyncJobItemStatus.Synced) + { + throw new ArgumentException("Operation is not valid for this job item"); + } + + jobItem.IsMarkedForRemoval = false; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + public async Task ReportSyncJobItemTransferBeginning(string id) + { + var jobItem = _repo.GetJobItem(id); + + jobItem.Status = SyncJobItemStatus.Transferring; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + public async Task ReportSyncJobItemTransferFailed(string id) + { + var jobItem = _repo.GetJobItem(id); + + jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + + await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); + + var processor = GetSyncJobProcessor(); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) + { + return _repo.GetSyncedItemProgresses(query); + } + + public SyncJobOptions GetAudioOptions(SyncJobItem jobItem, SyncJob job) + { + var options = GetSyncJobOptions(jobItem.TargetId, null, null); + + if (job.Bitrate.HasValue) + { + options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; + } + + return options; + } + + public ISyncProvider GetSyncProvider(SyncJobItem jobItem) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) + { + if (string.Equals(target.Id, jobItem.TargetId, StringComparison.OrdinalIgnoreCase)) + { + return provider; + } + } + } + return null; + } + + public SyncJobOptions GetVideoOptions(SyncJobItem jobItem, SyncJob job) + { + var options = GetSyncJobOptions(jobItem.TargetId, job.Profile, job.Quality); + + if (job.Bitrate.HasValue) + { + options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; + } + + return options; + } + + private SyncJobOptions GetSyncJobOptions(string targetId, string profile, string quality) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) + { + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetSyncJobOptions(provider, target, profile, quality); + } + } + } + + return GetDefaultSyncJobOptions(profile, quality); + } + + private SyncJobOptions GetSyncJobOptions(ISyncProvider provider, SyncTarget target, string profile, string quality) + { + var hasProfile = provider as IHasSyncQuality; + + if (hasProfile != null) + { + return hasProfile.GetSyncJobOptions(target, profile, quality); + } + + return GetDefaultSyncJobOptions(profile, quality); + } + + private SyncJobOptions GetDefaultSyncJobOptions(string profile, string quality) + { + var supportsAc3 = string.Equals(profile, "general", StringComparison.OrdinalIgnoreCase); + + var deviceProfile = new CloudSyncProfile(supportsAc3, false); + deviceProfile.MaxStaticBitrate = SyncHelper.AdjustBitrate(deviceProfile.MaxStaticBitrate, quality); + + return new SyncJobOptions + { + DeviceProfile = deviceProfile, + IsConverting = IsConverting(profile, quality) + }; + } + + private bool IsConverting(string profile, string quality) + { + return !string.Equals(profile, "original", StringComparison.OrdinalIgnoreCase); + } + + public IEnumerable GetQualityOptions(string targetId) + { + return GetQualityOptions(targetId, null); + } + + public IEnumerable GetQualityOptions(string targetId, User user) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) + { + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetQualityOptions(provider, target, user); + } + } + } + + return new List(); + } + + private IEnumerable GetQualityOptions(ISyncProvider provider, SyncTarget target, User user) + { + var hasQuality = provider as IHasSyncQuality; + if (hasQuality != null) + { + var options = hasQuality.GetQualityOptions(target); + + if (user != null && !user.Policy.EnableSyncTranscoding) + { + options = options.Where(i => i.IsOriginalQuality); + } + + return options; + } + + // Default options for providers that don't override + return new List + { + new SyncQualityOption + { + Name = "High", + Id = "high", + IsDefault = true + }, + new SyncQualityOption + { + Name = "Medium", + Id = "medium" + }, + new SyncQualityOption + { + Name = "Low", + Id = "low" + }, + new SyncQualityOption + { + Name = "Custom", + Id = "custom" + } + }; + } + + public IEnumerable GetProfileOptions(string targetId, User user) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) + { + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetProfileOptions(provider, target, user); + } + } + } + + return new List(); + } + + public IEnumerable GetProfileOptions(string targetId) + { + return GetProfileOptions(targetId, null); + } + + private IEnumerable GetProfileOptions(ISyncProvider provider, SyncTarget target, User user) + { + var hasQuality = provider as IHasSyncQuality; + if (hasQuality != null) + { + return hasQuality.GetProfileOptions(target); + } + + var list = new List(); + + list.Add(new SyncProfileOption + { + Name = "Original", + Id = "Original", + Description = "Syncs original files as-is.", + EnableQualityOptions = false + }); + + if (user == null || user.Policy.EnableSyncTranscoding) + { + list.Add(new SyncProfileOption + { + Name = "Baseline", + Id = "baseline", + Description = "Designed for compatibility with all devices, including web browsers. Targets H264/AAC video and MP3 audio." + }); + + list.Add(new SyncProfileOption + { + Name = "General", + Id = "general", + Description = "Designed for compatibility with Chromecast, Roku, Smart TV's, and other similar devices. Targets H264/AAC/AC3 video and MP3 audio.", + IsDefault = true + }); + } + + return list; + } + + protected internal void OnConversionComplete(SyncJobItem item) + { + var syncProvider = GetSyncProvider(item); + if (syncProvider is AppSyncProvider) + { + return; + } + + _taskManager.QueueIfNotRunning(); + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncNotificationEntryPoint.cs b/Emby.Server.Implementations/Sync/SyncNotificationEntryPoint.cs new file mode 100644 index 000000000..46cdb28a4 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncNotificationEntryPoint.cs @@ -0,0 +1,48 @@ +using System.Threading; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Sync; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncNotificationEntryPoint : IServerEntryPoint + { + private readonly ISessionManager _sessionManager; + private readonly ISyncManager _syncManager; + + public SyncNotificationEntryPoint(ISyncManager syncManager, ISessionManager sessionManager) + { + _syncManager = syncManager; + _sessionManager = sessionManager; + } + + public void Run() + { + _syncManager.SyncJobItemUpdated += _syncManager_SyncJobItemUpdated; + } + + private async void _syncManager_SyncJobItemUpdated(object sender, GenericEventArgs e) + { + var item = e.Argument; + + if (item.Status == SyncJobItemStatus.ReadyToTransfer) + { + try + { + await _sessionManager.SendMessageToUserDeviceSessions(item.TargetId, "SyncJobItemReady", item, CancellationToken.None).ConfigureAwait(false); + } + catch + { + + } + } + } + + public void Dispose() + { + _syncManager.SyncJobItemUpdated -= _syncManager_SyncJobItemUpdated; + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncRegistrationInfo.cs b/Emby.Server.Implementations/Sync/SyncRegistrationInfo.cs new file mode 100644 index 000000000..c2658c5c5 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncRegistrationInfo.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Common.Security; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncRegistrationInfo : IRequiresRegistration + { + private readonly ISecurityManager _securityManager; + + public static SyncRegistrationInfo Instance; + + public SyncRegistrationInfo(ISecurityManager securityManager) + { + _securityManager = securityManager; + Instance = this; + } + + private bool _registered; + public bool IsRegistered + { + get { return _registered; } + } + + public async Task LoadRegistrationInfoAsync() + { + var info = await _securityManager.GetRegistrationStatus("sync").ConfigureAwait(false); + + _registered = info.IsValid; + } + } +} diff --git a/Emby.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/Emby.Server.Implementations/Sync/SyncedMediaSourceProvider.cs new file mode 100644 index 000000000..1e54885e6 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncedMediaSourceProvider.cs @@ -0,0 +1,158 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncedMediaSourceProvider : IMediaSourceProvider + { + private readonly SyncManager _syncManager; + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + + public SyncedMediaSourceProvider(ISyncManager syncManager, IServerApplicationHost appHost, ILogger logger) + { + _appHost = appHost; + _logger = logger; + _syncManager = (SyncManager)syncManager; + } + + public async Task> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + { + var jobItemResult = _syncManager.GetJobItems(new SyncJobItemQuery + { + AddMetadata = false, + Statuses = new[] { SyncJobItemStatus.Synced }, + ItemId = item.Id.ToString("N") + }); + + var list = new List(); + + if (jobItemResult.Items.Length > 0) + { + var targets = _syncManager.ServerSyncProviders + .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple(i, t))) + .ToList(); + + var serverId = _appHost.SystemId; + + foreach (var jobItem in jobItemResult.Items) + { + var targetTuple = targets.FirstOrDefault(i => string.Equals(i.Item2.Id, jobItem.TargetId, StringComparison.OrdinalIgnoreCase)); + + if (targetTuple != null) + { + var syncTarget = targetTuple.Item2; + var syncProvider = targetTuple.Item1; + var dataProvider = _syncManager.GetDataProvider(targetTuple.Item1, syncTarget); + + var localItems = await dataProvider.GetItems(syncTarget, serverId, item.Id.ToString("N")).ConfigureAwait(false); + + foreach (var localItem in localItems) + { + foreach (var mediaSource in localItem.Item.MediaSources) + { + AddMediaSource(list, localItem, mediaSource, syncProvider, syncTarget); + } + } + } + } + } + + return list; + } + + private void AddMediaSource(List list, + LocalItem item, + MediaSourceInfo mediaSource, + IServerSyncProvider provider, + SyncTarget target) + { + SetStaticMediaSourceInfo(item, mediaSource); + + var requiresDynamicAccess = provider as IHasDynamicAccess; + + if (requiresDynamicAccess != null) + { + mediaSource.RequiresOpening = true; + + var keyList = new List(); + keyList.Add(provider.GetType().FullName.GetMD5().ToString("N")); + keyList.Add(target.Id.GetMD5().ToString("N")); + keyList.Add(item.Id); + mediaSource.OpenToken = string.Join(StreamIdDelimeterString, keyList.ToArray()); + } + + list.Add(mediaSource); + } + + // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. + private const string StreamIdDelimeterString = "_"; + + public async Task> OpenMediaSource(string openToken, CancellationToken cancellationToken) + { + var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3); + + var provider = _syncManager.ServerSyncProviders + .FirstOrDefault(i => string.Equals(openKeys[0], i.GetType().FullName.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase)); + + var target = provider.GetAllSyncTargets() + .FirstOrDefault(i => string.Equals(openKeys[1], i.Id.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase)); + + var dataProvider = _syncManager.GetDataProvider(provider, target); + var localItem = await dataProvider.Get(target, openKeys[2]).ConfigureAwait(false); + + var fileId = localItem.FileId; + if (string.IsNullOrWhiteSpace(fileId)) + { + } + + var requiresDynamicAccess = (IHasDynamicAccess)provider; + var dynamicInfo = await requiresDynamicAccess.GetSyncedFileInfo(fileId, target, cancellationToken).ConfigureAwait(false); + + var mediaSource = localItem.Item.MediaSources.First(); + mediaSource.LiveStreamId = Guid.NewGuid().ToString(); + SetStaticMediaSourceInfo(localItem, mediaSource); + + foreach (var stream in mediaSource.MediaStreams) + { + if (!string.IsNullOrWhiteSpace(stream.ExternalId)) + { + var dynamicStreamInfo = await requiresDynamicAccess.GetSyncedFileInfo(stream.ExternalId, target, cancellationToken).ConfigureAwait(false); + stream.Path = dynamicStreamInfo.Path; + } + } + + mediaSource.Path = dynamicInfo.Path; + mediaSource.Protocol = dynamicInfo.Protocol; + mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders; + + return new Tuple(mediaSource, null); + } + + private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource) + { + mediaSource.Id = item.Id; + mediaSource.SupportsTranscoding = false; + if (mediaSource.Protocol == MediaBrowser.Model.MediaInfo.MediaProtocol.File) + { + mediaSource.ETag = item.Id; + } + } + + public Task CloseMediaSource(string liveStreamId) + { + throw new NotImplementedException(); + } + } +} diff --git a/Emby.Server.Implementations/Sync/TargetDataProvider.cs b/Emby.Server.Implementations/Sync/TargetDataProvider.cs new file mode 100644 index 000000000..a0e0f4313 --- /dev/null +++ b/Emby.Server.Implementations/Sync/TargetDataProvider.cs @@ -0,0 +1,188 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Sync +{ + public class TargetDataProvider : ISyncDataProvider + { + private readonly SyncTarget _target; + private readonly IServerSyncProvider _provider; + + private readonly SemaphoreSlim _dataLock = new SemaphoreSlim(1, 1); + private List _items; + + private readonly ILogger _logger; + private readonly IJsonSerializer _json; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly IServerApplicationHost _appHost; + private readonly IMemoryStreamProvider _memoryStreamProvider; + + public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths, IMemoryStreamProvider memoryStreamProvider) + { + _logger = logger; + _json = json; + _provider = provider; + _target = target; + _fileSystem = fileSystem; + _appPaths = appPaths; + _memoryStreamProvider = memoryStreamProvider; + _appHost = appHost; + } + + private string[] GetRemotePath() + { + var parts = new List + { + _appHost.FriendlyName, + "data.json" + }; + + parts = parts.Select(i => GetValidFilename(_provider, i)).ToList(); + + return parts.ToArray(); + } + + private string GetValidFilename(IServerSyncProvider provider, string filename) + { + // We can always add this method to the sync provider if it's really needed + return _fileSystem.GetValidFilename(filename); + } + + private async Task> RetrieveItems(CancellationToken cancellationToken) + { + _logger.Debug("Getting {0} from {1}", string.Join(MediaSync.PathSeparatorString, GetRemotePath().ToArray()), _provider.Name); + + var fileResult = await _provider.GetFiles(GetRemotePath().ToArray(), _target, cancellationToken).ConfigureAwait(false); + + if (fileResult.Items.Length > 0) + { + using (var stream = await _provider.GetFile(fileResult.Items[0].FullName, _target, new Progress(), cancellationToken)) + { + return _json.DeserializeFromStream>(stream); + } + } + + return new List(); + } + + private async Task EnsureData(CancellationToken cancellationToken) + { + if (_items == null) + { + _items = await RetrieveItems(cancellationToken).ConfigureAwait(false); + } + } + + private async Task SaveData(List items, CancellationToken cancellationToken) + { + using (var stream = _memoryStreamProvider.CreateNew()) + { + _json.SerializeToStream(items, stream); + + // Save to sync provider + stream.Position = 0; + var remotePath = GetRemotePath(); + _logger.Debug("Saving data.json to {0}. Remote path: {1}", _provider.Name, string.Join("/", remotePath)); + + await _provider.SendFile(stream, remotePath, _target, new Progress(), cancellationToken).ConfigureAwait(false); + } + } + + private async Task GetData(bool enableCache, Func, T> dataFactory) + { + if (!enableCache) + { + var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false); + var newCache = items.ToList(); + var result = dataFactory(items); + await UpdateCache(newCache).ConfigureAwait(false); + return result; + } + + await _dataLock.WaitAsync().ConfigureAwait(false); + + try + { + await EnsureData(CancellationToken.None).ConfigureAwait(false); + + return dataFactory(_items); + } + finally + { + _dataLock.Release(); + } + } + + private async Task UpdateData(Func, List> action) + { + var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false); + items = action(items); + await SaveData(items.ToList(), CancellationToken.None).ConfigureAwait(false); + + await UpdateCache(null).ConfigureAwait(false); + } + + private async Task UpdateCache(List list) + { + await _dataLock.WaitAsync().ConfigureAwait(false); + + try + { + _items = list; + } + finally + { + _dataLock.Release(); + } + } + + public Task> GetLocalItems(SyncTarget target, string serverId) + { + return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase)).ToList()); + } + + public Task AddOrUpdate(SyncTarget target, LocalItem item) + { + return UpdateData(items => + { + var list = items.Where(i => !string.Equals(i.Id, item.Id, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + list.Add(item); + + return list; + }); + } + + public Task Delete(SyncTarget target, string id) + { + return UpdateData(items => items.Where(i => !string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)).ToList()); + } + + public Task Get(SyncTarget target, string id) + { + return GetData(true, items => items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase))); + } + + public Task> GetItems(SyncTarget target, string serverId, string itemId) + { + return GetData(true, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)).ToList()); + } + + public Task> GetItemsBySyncJobItemId(SyncTarget target, string serverId, string syncJobItemId) + { + return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.SyncJobItemId, syncJobItemId, StringComparison.OrdinalIgnoreCase)).ToList()); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 1248d138d..0baee67ea 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -793,7 +793,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (protocol == MediaProtocol.File) { - if (_textEncoding.GetFileEncoding(path).Equals(Encoding.UTF8)) + var fileEncoding = _textEncoding.GetFileEncoding(path); + + if (fileEncoding != null && fileEncoding.Equals(Encoding.UTF8)) { return string.Empty; } diff --git a/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs b/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs index 696a7c100..a3f86f9e2 100644 --- a/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptographyProvider.cs @@ -9,5 +9,6 @@ namespace MediaBrowser.Model.Cryptography byte[] GetMD5Bytes(string str); byte[] GetSHA1Bytes(byte[] bytes); byte[] GetMD5Bytes(Stream str); + byte[] GetMD5Bytes(byte[] bytes); } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 1f2a6255e..49cb1e75f 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -4,7 +4,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Logging; -using Microsoft.Win32; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -17,6 +16,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.IO; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Threading; +using Microsoft.Win32; namespace MediaBrowser.Server.Implementations.IO { diff --git a/MediaBrowser.Server.Implementations/IO/ThrottledStream.cs b/MediaBrowser.Server.Implementations/IO/ThrottledStream.cs deleted file mode 100644 index 538812dc0..000000000 --- a/MediaBrowser.Server.Implementations/IO/ThrottledStream.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.IO -{ - /// - /// Class for streaming data with throttling support. - /// - public class ThrottledStream : Stream - { - /// - /// A constant used to specify an infinite number of bytes that can be transferred per second. - /// - public const long Infinite = 0; - - #region Private members - /// - /// The base stream. - /// - private readonly Stream _baseStream; - - /// - /// The maximum bytes per second that can be transferred through the base stream. - /// - private long _maximumBytesPerSecond; - - /// - /// The number of bytes that has been transferred since the last throttle. - /// - private long _byteCount; - - /// - /// The start time in milliseconds of the last throttle. - /// - private long _start; - #endregion - - #region Properties - /// - /// Gets the current milliseconds. - /// - /// The current milliseconds. - protected long CurrentMilliseconds - { - get - { - return Environment.TickCount; - } - } - - /// - /// Gets or sets the maximum bytes per second that can be transferred through the base stream. - /// - /// The maximum bytes per second. - public long MaximumBytesPerSecond - { - get - { - return _maximumBytesPerSecond; - } - set - { - if (MaximumBytesPerSecond != value) - { - _maximumBytesPerSecond = value; - Reset(); - } - } - } - - /// - /// Gets a value indicating whether the current stream supports reading. - /// - /// true if the stream supports reading; otherwise, false. - public override bool CanRead - { - get - { - return _baseStream.CanRead; - } - } - - /// - /// Gets a value indicating whether the current stream supports seeking. - /// - /// - /// true if the stream supports seeking; otherwise, false. - public override bool CanSeek - { - get - { - return _baseStream.CanSeek; - } - } - - /// - /// Gets a value indicating whether the current stream supports writing. - /// - /// - /// true if the stream supports writing; otherwise, false. - public override bool CanWrite - { - get - { - return _baseStream.CanWrite; - } - } - - /// - /// Gets the length in bytes of the stream. - /// - /// - /// A long value representing the length of the stream in bytes. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Length - { - get - { - return _baseStream.Length; - } - } - - /// - /// Gets or sets the position within the current stream. - /// - /// - /// The current position within the stream. - /// An I/O error occurs. - /// The base stream does not support seeking. - /// Methods were called after the stream was closed. - public override long Position - { - get - { - return _baseStream.Position; - } - set - { - _baseStream.Position = value; - } - } - #endregion - - public long MinThrottlePosition; - - #region Ctor - /// - /// Initializes a new instance of the class. - /// - /// The base stream. - /// The maximum bytes per second that can be transferred through the base stream. - /// Thrown when is a null reference. - /// Thrown when is a negative value. - public ThrottledStream(Stream baseStream, long maximumBytesPerSecond) - { - if (baseStream == null) - { - throw new ArgumentNullException("baseStream"); - } - - if (maximumBytesPerSecond < 0) - { - throw new ArgumentOutOfRangeException("maximumBytesPerSecond", - maximumBytesPerSecond, "The maximum number of bytes per second can't be negative."); - } - - _baseStream = baseStream; - _maximumBytesPerSecond = maximumBytesPerSecond; - _start = CurrentMilliseconds; - _byteCount = 0; - } - #endregion - - #region Public methods - /// - /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. - /// - /// An I/O error occurs. - public override void Flush() - { - _baseStream.Flush(); - } - - /// - /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. - /// - /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source. - /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. - /// The maximum number of bytes to be read from the current stream. - /// - /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. - /// - /// The sum of offset and count is larger than the buffer length. - /// Methods were called after the stream was closed. - /// The base stream does not support reading. - /// buffer is null. - /// An I/O error occurs. - /// offset or count is negative. - public override int Read(byte[] buffer, int offset, int count) - { - Throttle(count); - - return _baseStream.Read(buffer, offset, count); - } - - /// - /// Sets the position within the current stream. - /// - /// A byte offset relative to the origin parameter. - /// A value of type indicating the reference point used to obtain the new position. - /// - /// The new position within the current stream. - /// - /// An I/O error occurs. - /// The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. - /// Methods were called after the stream was closed. - public override long Seek(long offset, SeekOrigin origin) - { - return _baseStream.Seek(offset, origin); - } - - /// - /// Sets the length of the current stream. - /// - /// The desired length of the current stream in bytes. - /// The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. - /// An I/O error occurs. - /// Methods were called after the stream was closed. - public override void SetLength(long value) - { - _baseStream.SetLength(value); - } - - private long _bytesWritten; - - /// - /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. - /// - /// An array of bytes. This method copies count bytes from buffer to the current stream. - /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. - /// The number of bytes to be written to the current stream. - /// An I/O error occurs. - /// The base stream does not support writing. - /// Methods were called after the stream was closed. - /// buffer is null. - /// The sum of offset and count is greater than the buffer length. - /// offset or count is negative. - public override void Write(byte[] buffer, int offset, int count) - { - Throttle(count); - - _baseStream.Write(buffer, offset, count); - - _bytesWritten += count; - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - await ThrottleAsync(count, cancellationToken).ConfigureAwait(false); - - await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - - _bytesWritten += count; - } - - /// - /// Returns a that represents the current . - /// - /// - /// A that represents the current . - /// - public override string ToString() - { - return _baseStream.ToString(); - } - #endregion - - private bool ThrottleCheck(int bufferSizeInBytes) - { - if (_bytesWritten < MinThrottlePosition) - { - return false; - } - - // Make sure the buffer isn't empty. - if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) - { - return false; - } - - return true; - } - - #region Protected methods - /// - /// Throttles for the specified buffer size in bytes. - /// - /// The buffer size in bytes. - protected void Throttle(int bufferSizeInBytes) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return ; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - try - { - // The time to sleep is more then a millisecond, so sleep. - Thread.Sleep(toSleep); - } - catch (ThreadAbortException) - { - // Eatup ThreadAbortException. - } - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken) - { - if (!ThrottleCheck(bufferSizeInBytes)) - { - return; - } - - _byteCount += bufferSizeInBytes; - long elapsedMilliseconds = CurrentMilliseconds - _start; - - if (elapsedMilliseconds > 0) - { - // Calculate the current bps. - long bps = _byteCount * 1000L / elapsedMilliseconds; - - // If the bps are more then the maximum bps, try to throttle. - if (bps > _maximumBytesPerSecond) - { - // Calculate the time to sleep. - long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; - int toSleep = (int)(wakeElapsed - elapsedMilliseconds); - - if (toSleep > 1) - { - // The time to sleep is more then a millisecond, so sleep. - await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false); - - // A sleep has been done, reset. - Reset(); - } - } - } - } - - /// - /// Will reset the bytecount to 0 and reset the start time to the current time. - /// - protected void Reset() - { - long difference = CurrentMilliseconds - _start; - - // Only reset counters when a known history is available of more then 1 second. - if (difference > 1000) - { - _byteCount = 0; - _start = CurrentMilliseconds; - } - } - #endregion - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index b31c8286c..5d1d2fc38 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -144,7 +144,6 @@ - @@ -202,17 +201,12 @@ - - - - - @@ -235,23 +229,9 @@ - - - - - - - - - - - - - - diff --git a/MediaBrowser.Server.Implementations/Reflection/AssemblyInfo.cs b/MediaBrowser.Server.Implementations/Reflection/AssemblyInfo.cs deleted file mode 100644 index ec25e7951..000000000 --- a/MediaBrowser.Server.Implementations/Reflection/AssemblyInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.IO; -using MediaBrowser.Model.Reflection; - -namespace MediaBrowser.Server.Implementations.Reflection -{ - public class AssemblyInfo : IAssemblyInfo - { - public Stream GetManifestResourceStream(Type type, string resource) - { - return type.Assembly.GetManifestResourceStream(resource); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs deleted file mode 100644 index 408ec717e..000000000 --- a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs +++ /dev/null @@ -1,118 +0,0 @@ -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds, IHasSyncQuality, IHasDuplicateCheck - { - private readonly IDeviceManager _deviceManager; - - public AppSyncProvider(IDeviceManager deviceManager) - { - _deviceManager = deviceManager; - } - - public IEnumerable GetSyncTargets(string userId) - { - return _deviceManager.GetDevices(new DeviceQuery - { - SupportsSync = true, - UserId = userId - - }).Items.Select(i => new SyncTarget - { - Id = i.Id, - Name = i.Name - }); - } - - public DeviceProfile GetDeviceProfile(SyncTarget target, string profile, string quality) - { - var caps = _deviceManager.GetCapabilities(target.Id); - - var deviceProfile = caps == null || caps.DeviceProfile == null ? new DeviceProfile() : caps.DeviceProfile; - deviceProfile.MaxStaticBitrate = SyncHelper.AdjustBitrate(deviceProfile.MaxStaticBitrate, quality); - - return deviceProfile; - } - - public string Name - { - get { return "Mobile Sync"; } - } - - public IEnumerable GetAllSyncTargets() - { - return _deviceManager.GetDevices(new DeviceQuery - { - SupportsSync = true - - }).Items.Select(i => new SyncTarget - { - Id = i.Id, - Name = i.Name - }); - } - - public IEnumerable GetQualityOptions(SyncTarget target) - { - return new List - { - new SyncQualityOption - { - Name = "Original", - Id = "original", - Description = "Syncs original files as-is, regardless of whether the device is capable of playing them or not." - }, - new SyncQualityOption - { - Name = "High", - Id = "high", - IsDefault = true - }, - new SyncQualityOption - { - Name = "Medium", - Id = "medium" - }, - new SyncQualityOption - { - Name = "Low", - Id = "low" - }, - new SyncQualityOption - { - Name = "Custom", - Id = "custom" - } - }; - } - - public IEnumerable GetProfileOptions(SyncTarget target) - { - return new List(); - } - - public SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality) - { - var isConverting = !string.Equals(quality, "original", StringComparison.OrdinalIgnoreCase); - - return new SyncJobOptions - { - DeviceProfile = GetDeviceProfile(target, profile, quality), - IsConverting = isConverting - }; - } - - public bool AllowDuplicateJobItem(SyncJobItem original, SyncJobItem duplicate) - { - return false; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs b/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs deleted file mode 100644 index f40b64498..000000000 --- a/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs +++ /dev/null @@ -1,302 +0,0 @@ -using MediaBrowser.Model.Dlna; -using System.Collections.Generic; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class CloudSyncProfile : DeviceProfile - { - public CloudSyncProfile(bool supportsAc3, bool supportsDca) - { - Name = "Cloud Sync"; - - MaxStreamingBitrate = 20000000; - MaxStaticBitrate = 20000000; - - var mkvAudio = "aac,mp3"; - var mp4Audio = "aac"; - - if (supportsAc3) - { - mkvAudio += ",ac3"; - mp4Audio += ",ac3"; - } - - if (supportsDca) - { - mkvAudio += ",dca,dts"; - } - - var videoProfile = "high|main|baseline|constrained baseline"; - var videoLevel = "40"; - - DirectPlayProfiles = new[] - { - //new DirectPlayProfile - //{ - // Container = "mkv", - // VideoCodec = "h264,mpeg4", - // AudioCodec = mkvAudio, - // Type = DlnaProfileType.Video - //}, - new DirectPlayProfile - { - Container = "mp4,mov,m4v", - VideoCodec = "h264,mpeg4", - AudioCodec = mp4Audio, - Type = DlnaProfileType.Video - }, - new DirectPlayProfile - { - Container = "mp3", - Type = DlnaProfileType.Audio - } - }; - - ContainerProfiles = new[] - { - new ContainerProfile - { - Type = DlnaProfileType.Video, - Conditions = new [] - { - new ProfileCondition - { - Condition = ProfileConditionType.NotEquals, - Property = ProfileConditionValue.NumAudioStreams, - Value = "0", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.EqualsAny, - Property = ProfileConditionValue.NumVideoStreams, - Value = "1", - IsRequired = false - } - } - } - }; - - var codecProfiles = new List - { - new CodecProfile - { - Type = CodecType.Video, - Codec = "h264", - Conditions = new [] - { - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.VideoBitDepth, - Value = "8", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.Width, - Value = "1920", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.Height, - Value = "1080", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.RefFrames, - Value = "4", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.VideoFramerate, - Value = "30", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.Equals, - Property = ProfileConditionValue.IsAnamorphic, - Value = "false", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.VideoLevel, - Value = videoLevel, - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.EqualsAny, - Property = ProfileConditionValue.VideoProfile, - Value = videoProfile, - IsRequired = false - } - } - }, - new CodecProfile - { - Type = CodecType.Video, - Codec = "mpeg4", - Conditions = new [] - { - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.VideoBitDepth, - Value = "8", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.Width, - Value = "1920", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.Height, - Value = "1080", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.RefFrames, - Value = "4", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.VideoFramerate, - Value = "30", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.Equals, - Property = ProfileConditionValue.IsAnamorphic, - Value = "false", - IsRequired = false - } - } - } - }; - - codecProfiles.Add(new CodecProfile - { - Type = CodecType.VideoAudio, - Codec = "ac3", - Conditions = new[] - { - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.AudioChannels, - Value = "6", - IsRequired = false - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.AudioBitrate, - Value = "320000", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.Equals, - Property = ProfileConditionValue.IsSecondaryAudio, - Value = "false", - IsRequired = false - } - } - }); - codecProfiles.Add(new CodecProfile - { - Type = CodecType.VideoAudio, - Codec = "aac,mp3", - Conditions = new[] - { - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.AudioChannels, - Value = "2", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.LessThanEqual, - Property = ProfileConditionValue.AudioBitrate, - Value = "320000", - IsRequired = true - }, - new ProfileCondition - { - Condition = ProfileConditionType.Equals, - Property = ProfileConditionValue.IsSecondaryAudio, - Value = "false", - IsRequired = false - } - } - }); - - CodecProfiles = codecProfiles.ToArray(); - - SubtitleProfiles = new[] - { - new SubtitleProfile - { - Format = "srt", - Method = SubtitleDeliveryMethod.External - }, - new SubtitleProfile - { - Format = "vtt", - Method = SubtitleDeliveryMethod.External - } - }; - - TranscodingProfiles = new[] - { - new TranscodingProfile - { - Container = "mp3", - AudioCodec = "mp3", - Type = DlnaProfileType.Audio, - Context = EncodingContext.Static - }, - - new TranscodingProfile - { - Container = "mp4", - Type = DlnaProfileType.Video, - AudioCodec = "aac", - VideoCodec = "h264", - Context = EncodingContext.Static - }, - - new TranscodingProfile - { - Container = "jpeg", - Type = DlnaProfileType.Photo, - Context = EncodingContext.Static - } - }; - - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs b/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs deleted file mode 100644 index e7eee0923..000000000 --- a/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediaBrowser.Model.Sync; -using System.Collections.Generic; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public interface IHasSyncQuality - { - /// - /// Gets the device profile. - /// - /// The target. - /// The profile. - /// The quality. - /// DeviceProfile. - SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality); - - /// - /// Gets the quality options. - /// - /// The target. - /// IEnumerable<SyncQualityOption>. - IEnumerable GetQualityOptions(SyncTarget target); - - /// - /// Gets the profile options. - /// - /// The target. - /// IEnumerable<SyncQualityOption>. - IEnumerable GetProfileOptions(SyncTarget target); - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs b/MediaBrowser.Server.Implementations/Sync/MediaSync.cs deleted file mode 100644 index b6853267e..000000000 --- a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs +++ /dev/null @@ -1,502 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; -using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Common.IO; -using MediaBrowser.Server.Implementations.IO; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class MediaSync - { - private readonly ISyncManager _syncManager; - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IConfigurationManager _config; - - public const string PathSeparatorString = "/"; - public const char PathSeparatorChar = '/'; - - public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem, IConfigurationManager config) - { - _logger = logger; - _syncManager = syncManager; - _appHost = appHost; - _fileSystem = fileSystem; - _config = config; - } - - public async Task Sync(IServerSyncProvider provider, - ISyncDataProvider dataProvider, - SyncTarget target, - IProgress progress, - CancellationToken cancellationToken) - { - var serverId = _appHost.SystemId; - var serverName = _appHost.FriendlyName; - - await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); - progress.Report(3); - - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(pct => - { - var totalProgress = pct * .97; - totalProgress += 1; - progress.Report(totalProgress); - }); - await GetNewMedia(provider, dataProvider, target, serverId, serverName, innerProgress, cancellationToken); - - // Do the data sync twice so the server knows what was removed from the device - await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); - - progress.Report(100); - } - - private async Task SyncData(IServerSyncProvider provider, - ISyncDataProvider dataProvider, - string serverId, - SyncTarget target, - CancellationToken cancellationToken) - { - var localItems = await dataProvider.GetLocalItems(target, serverId).ConfigureAwait(false); - var remoteFiles = await provider.GetFiles(target, cancellationToken).ConfigureAwait(false); - var remoteIds = remoteFiles.Items.Select(i => i.FullName).ToList(); - - var jobItemIds = new List(); - - foreach (var localItem in localItems) - { - if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase)) - { - jobItemIds.Add(localItem.SyncJobItemId); - } - } - - var result = await _syncManager.SyncData(new SyncDataRequest - { - TargetId = target.Id, - SyncJobItemIds = jobItemIds - - }).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - foreach (var itemIdToRemove in result.ItemIdsToRemove) - { - try - { - await RemoveItem(provider, dataProvider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove); - } - } - } - - private async Task GetNewMedia(IServerSyncProvider provider, - ISyncDataProvider dataProvider, - SyncTarget target, - string serverId, - string serverName, - IProgress progress, - CancellationToken cancellationToken) - { - var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false); - - var numComplete = 0; - double startingPercent = 0; - double percentPerItem = 1; - if (jobItems.Count > 0) - { - percentPerItem /= jobItems.Count; - } - - foreach (var jobItem in jobItems) - { - cancellationToken.ThrowIfCancellationRequested(); - - var currentPercent = startingPercent; - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(pct => - { - var totalProgress = pct * percentPerItem; - totalProgress += currentPercent; - progress.Report(totalProgress); - }); - - try - { - await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error syncing item", ex); - } - - numComplete++; - startingPercent = numComplete; - startingPercent /= jobItems.Count; - startingPercent *= 100; - progress.Report(startingPercent); - } - } - - private async Task GetItem(IServerSyncProvider provider, - ISyncDataProvider dataProvider, - SyncTarget target, - string serverId, - string serverName, - SyncedItem jobItem, - IProgress progress, - CancellationToken cancellationToken) - { - var libraryItem = jobItem.Item; - var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId); - var internalSyncJob = _syncManager.GetJob(jobItem.SyncJobId); - - var localItem = CreateLocalItem(provider, jobItem, internalSyncJob, target, libraryItem, serverId, serverName, jobItem.OriginalFileName); - - await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id); - - var transferSuccess = false; - Exception transferException = null; - - var options = _config.GetSyncOptions(); - - try - { - var fileTransferProgress = new ActionableProgress(); - fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92)); - - var sendFileResult = await SendFile(provider, internalSyncJobItem.OutputPath, localItem.LocalPath.Split(PathSeparatorChar), target, options, fileTransferProgress, cancellationToken).ConfigureAwait(false); - - if (localItem.Item.MediaSources != null) - { - var mediaSource = localItem.Item.MediaSources.FirstOrDefault(); - if (mediaSource != null) - { - mediaSource.Path = sendFileResult.Path; - mediaSource.Protocol = sendFileResult.Protocol; - mediaSource.RequiredHttpHeaders = sendFileResult.RequiredHttpHeaders; - mediaSource.SupportsTranscoding = false; - } - } - - localItem.FileId = sendFileResult.Id; - - // Create db record - await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); - - if (localItem.Item.MediaSources != null) - { - var mediaSource = localItem.Item.MediaSources.FirstOrDefault(); - if (mediaSource != null) - { - await SendSubtitles(localItem, mediaSource, provider, dataProvider, target, options, cancellationToken).ConfigureAwait(false); - } - } - - progress.Report(92); - - transferSuccess = true; - - progress.Report(99); - } - catch (Exception ex) - { - _logger.ErrorException("Error transferring sync job file", ex); - transferException = ex; - } - - if (transferSuccess) - { - await _syncManager.ReportSyncJobItemTransferred(jobItem.SyncJobItemId).ConfigureAwait(false); - } - else - { - await _syncManager.ReportSyncJobItemTransferFailed(jobItem.SyncJobItemId).ConfigureAwait(false); - - throw transferException; - } - } - - private async Task SendSubtitles(LocalItem localItem, MediaSourceInfo mediaSource, IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, SyncOptions options, CancellationToken cancellationToken) - { - var failedSubtitles = new List(); - var requiresSave = false; - - foreach (var mediaStream in mediaSource.MediaStreams - .Where(i => i.Type == MediaStreamType.Subtitle && i.IsExternal) - .ToList()) - { - try - { - var remotePath = GetRemoteSubtitlePath(localItem, mediaStream, provider, target); - var sendFileResult = await SendFile(provider, mediaStream.Path, remotePath, target, options, new Progress(), cancellationToken).ConfigureAwait(false); - - // This is the path that will be used when talking to the provider - mediaStream.ExternalId = sendFileResult.Id; - - // Keep track of all additional files for cleanup later. - localItem.AdditionalFiles.Add(sendFileResult.Id); - - // This is the public path clients will use - mediaStream.Path = sendFileResult.Path; - requiresSave = true; - } - catch (Exception ex) - { - _logger.ErrorException("Error sending subtitle stream", ex); - failedSubtitles.Add(mediaStream); - } - } - - if (failedSubtitles.Count > 0) - { - mediaSource.MediaStreams = mediaSource.MediaStreams.Except(failedSubtitles).ToList(); - requiresSave = true; - } - - if (requiresSave) - { - await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); - } - } - - private string[] GetRemoteSubtitlePath(LocalItem item, MediaStream stream, IServerSyncProvider provider, SyncTarget target) - { - var filename = GetSubtitleSaveFileName(item, stream.Language, stream.IsForced) + "." + stream.Codec.ToLower(); - - var pathParts = item.LocalPath.Split(PathSeparatorChar); - var list = pathParts.Take(pathParts.Length - 1).ToList(); - list.Add(filename); - - return list.ToArray(); - } - - private string GetSubtitleSaveFileName(LocalItem item, string language, bool isForced) - { - var path = item.LocalPath; - - var name = Path.GetFileNameWithoutExtension(path); - - if (!string.IsNullOrWhiteSpace(language)) - { - name += "." + language.ToLower(); - } - - if (isForced) - { - name += ".foreign"; - } - - return name; - } - - private async Task RemoveItem(IServerSyncProvider provider, - ISyncDataProvider dataProvider, - string serverId, - string syncJobItemId, - SyncTarget target, - CancellationToken cancellationToken) - { - var localItems = await dataProvider.GetItemsBySyncJobItemId(target, serverId, syncJobItemId); - - foreach (var localItem in localItems) - { - var files = localItem.AdditionalFiles.ToList(); - - foreach (var file in files) - { - _logger.Debug("Removing {0} from {1}.", file, target.Name); - await provider.DeleteFile(file, target, cancellationToken).ConfigureAwait(false); - } - - _logger.Debug("Removing {0} from {1}.", localItem.FileId, target.Name); - await provider.DeleteFile(localItem.FileId, target, cancellationToken).ConfigureAwait(false); - - await dataProvider.Delete(target, localItem.Id).ConfigureAwait(false); - } - } - - private async Task SendFile(IServerSyncProvider provider, string inputPath, string[] pathParts, SyncTarget target, SyncOptions options, IProgress progress, CancellationToken cancellationToken) - { - _logger.Debug("Sending {0} to {1}. Remote path: {2}", inputPath, provider.Name, string.Join("/", pathParts)); - var supportsDirectCopy = provider as ISupportsDirectCopy; - if (supportsDirectCopy != null) - { - return await supportsDirectCopy.SendFile(inputPath, pathParts, target, progress, cancellationToken).ConfigureAwait(false); - } - - using (var fileStream = _fileSystem.GetFileStream(inputPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) - { - Stream stream = fileStream; - - if (options.UploadSpeedLimitBytes > 0 && provider is IRemoteSyncProvider) - { - stream = new ThrottledStream(stream, options.UploadSpeedLimitBytes); - } - - return await provider.SendFile(stream, pathParts, target, progress, cancellationToken).ConfigureAwait(false); - } - } - - private static string GetLocalId(string jobItemId, string itemId) - { - var bytes = Encoding.UTF8.GetBytes(jobItemId + itemId); - bytes = CreateMd5(bytes); - return BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty); - } - - private static byte[] CreateMd5(byte[] value) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(value); - } - } - - public LocalItem CreateLocalItem(IServerSyncProvider provider, SyncedItem syncedItem, SyncJob job, SyncTarget target, BaseItemDto libraryItem, string serverId, string serverName, string originalFileName) - { - var path = GetDirectoryPath(provider, job, syncedItem, libraryItem, serverName); - path.Add(GetLocalFileName(provider, libraryItem, originalFileName)); - - var localPath = string.Join(PathSeparatorString, path.ToArray()); - - foreach (var mediaSource in libraryItem.MediaSources) - { - mediaSource.Path = localPath; - mediaSource.Protocol = MediaProtocol.File; - } - - return new LocalItem - { - Item = libraryItem, - ItemId = libraryItem.Id, - ServerId = serverId, - LocalPath = localPath, - Id = GetLocalId(syncedItem.SyncJobItemId, libraryItem.Id), - SyncJobItemId = syncedItem.SyncJobItemId - }; - } - - private List GetDirectoryPath(IServerSyncProvider provider, SyncJob job, SyncedItem syncedItem, BaseItemDto item, string serverName) - { - var parts = new List - { - serverName - }; - - var profileOption = _syncManager.GetProfileOptions(job.TargetId) - .FirstOrDefault(i => string.Equals(i.Id, job.Profile, StringComparison.OrdinalIgnoreCase)); - - string name; - - if (profileOption != null && !string.IsNullOrWhiteSpace(profileOption.Name)) - { - name = profileOption.Name; - - if (job.Bitrate.HasValue) - { - name += "-" + job.Bitrate.Value.ToString(CultureInfo.InvariantCulture); - } - else - { - var qualityOption = _syncManager.GetQualityOptions(job.TargetId) - .FirstOrDefault(i => string.Equals(i.Id, job.Quality, StringComparison.OrdinalIgnoreCase)); - - if (qualityOption != null && !string.IsNullOrWhiteSpace(qualityOption.Name)) - { - name += "-" + qualityOption.Name; - } - } - } - else - { - name = syncedItem.SyncJobName + "-" + syncedItem.SyncJobDateCreated - .ToLocalTime() - .ToString("g") - .Replace(" ", "-"); - } - - name = GetValidFilename(provider, name); - parts.Add(name); - - if (item.IsType("episode")) - { - parts.Add("TV"); - if (!string.IsNullOrWhiteSpace(item.SeriesName)) - { - parts.Add(item.SeriesName); - } - } - else if (item.IsVideo) - { - parts.Add("Videos"); - parts.Add(item.Name); - } - else if (item.IsAudio) - { - parts.Add("Music"); - - if (!string.IsNullOrWhiteSpace(item.AlbumArtist)) - { - parts.Add(item.AlbumArtist); - } - - if (!string.IsNullOrWhiteSpace(item.Album)) - { - parts.Add(item.Album); - } - } - else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) - { - parts.Add("Photos"); - - if (!string.IsNullOrWhiteSpace(item.Album)) - { - parts.Add(item.Album); - } - } - - return parts.Select(i => GetValidFilename(provider, i)).ToList(); - } - - private string GetLocalFileName(IServerSyncProvider provider, BaseItemDto item, string originalFileName) - { - var filename = originalFileName; - - if (string.IsNullOrWhiteSpace(filename)) - { - filename = item.Name; - } - - return GetValidFilename(provider, filename); - } - - private string GetValidFilename(IServerSyncProvider provider, string filename) - { - // We can always add this method to the sync provider if it's really needed - return _fileSystem.GetValidFilename(filename); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs b/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs deleted file mode 100644 index 471604117..000000000 --- a/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs +++ /dev/null @@ -1,76 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class MultiProviderSync - { - private readonly SyncManager _syncManager; - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IConfigurationManager _config; - - public MultiProviderSync(SyncManager syncManager, IServerApplicationHost appHost, ILogger logger, IFileSystem fileSystem, IConfigurationManager config) - { - _syncManager = syncManager; - _appHost = appHost; - _logger = logger; - _fileSystem = fileSystem; - _config = config; - } - - public async Task Sync(IEnumerable providers, IProgress progress, CancellationToken cancellationToken) - { - var targets = providers - .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple(i, t))) - .ToList(); - - var numComplete = 0; - double startingPercent = 0; - double percentPerItem = 1; - if (targets.Count > 0) - { - percentPerItem /= targets.Count; - } - - foreach (var target in targets) - { - cancellationToken.ThrowIfCancellationRequested(); - - var currentPercent = startingPercent; - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(pct => - { - var totalProgress = pct * percentPerItem; - totalProgress += currentPercent; - progress.Report(totalProgress); - }); - - var dataProvider = _syncManager.GetDataProvider(target.Item1, target.Item2); - - await new MediaSync(_logger, _syncManager, _appHost, _fileSystem, _config) - .Sync(target.Item1, dataProvider, target.Item2, innerProgress, cancellationToken) - .ConfigureAwait(false); - - numComplete++; - startingPercent = numComplete; - startingPercent /= targets.Count; - startingPercent *= 100; - progress.Report(startingPercent); - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs deleted file mode 100644 index dc7f925a0..000000000 --- a/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs +++ /dev/null @@ -1,92 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; - -namespace MediaBrowser.Server.Implementations.Sync -{ - class ServerSyncScheduledTask : IScheduledTask, IConfigurableScheduledTask - { - private readonly ISyncManager _syncManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IServerApplicationHost _appHost; - private readonly IConfigurationManager _config; - - public ServerSyncScheduledTask(ISyncManager syncManager, ILogger logger, IFileSystem fileSystem, IServerApplicationHost appHost, IConfigurationManager config) - { - _syncManager = syncManager; - _logger = logger; - _fileSystem = fileSystem; - _appHost = appHost; - _config = config; - } - - public string Name - { - get { return "Cloud & Folder Sync"; } - } - - public string Description - { - get { return "Sync media to the cloud"; } - } - - public string Category - { - get - { - return "Sync"; - } - } - - public Task Execute(CancellationToken cancellationToken, IProgress progress) - { - return new MultiProviderSync((SyncManager)_syncManager, _appHost, _logger, _fileSystem, _config) - .Sync(ServerSyncProviders, progress, cancellationToken); - } - - public IEnumerable ServerSyncProviders - { - get { return ((SyncManager)_syncManager).ServerSyncProviders; } - } - - /// - /// Creates the triggers that define when the task will run - /// - public IEnumerable GetDefaultTriggers() - { - return new[] { - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks} - }; - } - public bool IsHidden - { - get { return !IsEnabled; } - } - - public bool IsEnabled - { - get { return ServerSyncProviders.Any(); } - } - - public bool IsLogged - { - get { return true; } - } - - public string Key - { - get { return "ServerSync"; } - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncConfig.cs b/MediaBrowser.Server.Implementations/Sync/SyncConfig.cs deleted file mode 100644 index 52c774330..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncConfig.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Sync; -using System.Collections.Generic; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncConfigurationFactory : IConfigurationFactory - { - public IEnumerable GetConfigurations() - { - return new List - { - new ConfigurationStore - { - ConfigurationType = typeof(SyncOptions), - Key = "sync" - } - }; - } - } - - public static class SyncExtensions - { - public static SyncOptions GetSyncOptions(this IConfigurationManager config) - { - return config.GetConfiguration("sync"); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs deleted file mode 100644 index 3a5023fe5..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs +++ /dev/null @@ -1,89 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncConvertScheduledTask : IScheduledTask - { - private readonly ILibraryManager _libraryManager; - private readonly ISyncRepository _syncRepo; - private readonly ISyncManager _syncManager; - private readonly ILogger _logger; - private readonly IUserManager _userManager; - private readonly ITVSeriesManager _tvSeriesManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly IMediaSourceManager _mediaSourceManager; - - public SyncConvertScheduledTask(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) - { - _libraryManager = libraryManager; - _syncRepo = syncRepo; - _syncManager = syncManager; - _logger = logger; - _userManager = userManager; - _tvSeriesManager = tvSeriesManager; - _mediaEncoder = mediaEncoder; - _subtitleEncoder = subtitleEncoder; - _config = config; - _fileSystem = fileSystem; - _mediaSourceManager = mediaSourceManager; - } - - public string Name - { - get { return "Convert media"; } - } - - public string Description - { - get { return "Runs scheduled sync jobs"; } - } - - public string Category - { - get - { - return "Sync"; - } - } - - public Task Execute(CancellationToken cancellationToken, IProgress progress) - { - return new SyncJobProcessor(_libraryManager, _syncRepo, (SyncManager)_syncManager, _logger, _userManager, _tvSeriesManager, _mediaEncoder, _subtitleEncoder, _config, _fileSystem, _mediaSourceManager) - .Sync(progress, cancellationToken); - } - - /// - /// Creates the triggers that define when the task will run - /// - /// IEnumerable{BaseTaskTrigger}. - public IEnumerable GetDefaultTriggers() - { - return new[] { - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(3).Ticks} - }; - } - - public string Key - { - get { return "SyncPrepare"; } - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs b/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs deleted file mode 100644 index fb4e0c6be..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncHelper - { - public static int? AdjustBitrate(int? profileBitrate, string quality) - { - if (profileBitrate.HasValue) - { - if (string.Equals(quality, "medium", StringComparison.OrdinalIgnoreCase)) - { - profileBitrate = Math.Min(profileBitrate.Value, 4000000); - } - else if (string.Equals(quality, "low", StringComparison.OrdinalIgnoreCase)) - { - profileBitrate = Math.Min(profileBitrate.Value, 1500000); - } - } - - return profileBitrate; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs deleted file mode 100644 index cb8141c89..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using MediaBrowser.Model.Dlna; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncJobOptions - { - /// - /// Gets or sets the conversion options. - /// - /// The conversion options. - public DeviceProfile DeviceProfile { get; set; } - /// - /// Gets or sets a value indicating whether this instance is converting. - /// - /// true if this instance is converting; otherwise, false. - public bool IsConverting { get; set; } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs deleted file mode 100644 index 8d2c0b20c..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs +++ /dev/null @@ -1,988 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.IO; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Extensions; -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncJobProcessor - { - private readonly ILibraryManager _libraryManager; - private readonly ISyncRepository _syncRepo; - private readonly SyncManager _syncManager; - private readonly ILogger _logger; - private readonly IUserManager _userManager; - private readonly ITVSeriesManager _tvSeriesManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly ISubtitleEncoder _subtitleEncoder; - private readonly IConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly IMediaSourceManager _mediaSourceManager; - - public SyncJobProcessor(ILibraryManager libraryManager, ISyncRepository syncRepo, SyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) - { - _libraryManager = libraryManager; - _syncRepo = syncRepo; - _syncManager = syncManager; - _logger = logger; - _userManager = userManager; - _tvSeriesManager = tvSeriesManager; - _mediaEncoder = mediaEncoder; - _subtitleEncoder = subtitleEncoder; - _config = config; - _fileSystem = fileSystem; - _mediaSourceManager = mediaSourceManager; - } - - public async Task EnsureJobItems(SyncJob job) - { - var user = _userManager.GetUserById(job.UserId); - - if (user == null) - { - throw new InvalidOperationException("Cannot proceed with sync because user no longer exists."); - } - - var items = (await GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false)) - .ToList(); - - var jobItems = _syncManager.GetJobItems(new SyncJobItemQuery - { - JobId = job.Id, - AddMetadata = false - - }).Items.ToList(); - - foreach (var item in items) - { - // Respect ItemLimit, if set - if (job.ItemLimit.HasValue) - { - if (jobItems.Count(j => j.Status != SyncJobItemStatus.RemovedFromDevice && j.Status != SyncJobItemStatus.Failed) >= job.ItemLimit.Value) - { - break; - } - } - - var itemId = item.Id.ToString("N"); - - var jobItem = jobItems.FirstOrDefault(i => string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)); - - if (jobItem != null) - { - continue; - } - - var index = jobItems.Count == 0 ? - 0 : - jobItems.Select(i => i.JobItemIndex).Max() + 1; - - jobItem = new SyncJobItem - { - Id = Guid.NewGuid().ToString("N"), - ItemId = itemId, - ItemName = GetSyncJobItemName(item), - JobId = job.Id, - TargetId = job.TargetId, - DateCreated = DateTime.UtcNow, - JobItemIndex = index - }; - - await _syncRepo.Create(jobItem).ConfigureAwait(false); - _syncManager.OnSyncJobItemCreated(jobItem); - - jobItems.Add(jobItem); - } - - jobItems = jobItems - .OrderBy(i => i.DateCreated) - .ToList(); - - await UpdateJobStatus(job, jobItems).ConfigureAwait(false); - } - - private string GetSyncJobItemName(BaseItem item) - { - var name = item.Name; - var episode = item as Episode; - - if (episode != null) - { - if (episode.IndexNumber.HasValue) - { - name = "E" + episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture) + " - " + name; - } - - if (episode.ParentIndexNumber.HasValue) - { - name = "S" + episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture) + ", " + name; - } - } - - return name; - } - - public Task UpdateJobStatus(string id) - { - var job = _syncRepo.GetJob(id); - - if (job == null) - { - return Task.FromResult(true); - } - - var result = _syncManager.GetJobItems(new SyncJobItemQuery - { - JobId = job.Id, - AddMetadata = false - }); - - return UpdateJobStatus(job, result.Items.ToList()); - } - - private async Task UpdateJobStatus(SyncJob job, List jobItems) - { - job.ItemCount = jobItems.Count; - - double pct = 0; - - foreach (var item in jobItems) - { - if (item.Status == SyncJobItemStatus.Failed || item.Status == SyncJobItemStatus.Synced || item.Status == SyncJobItemStatus.RemovedFromDevice || item.Status == SyncJobItemStatus.Cancelled) - { - pct += 100; - } - else - { - pct += item.Progress ?? 0; - } - } - - if (job.ItemCount > 0) - { - pct /= job.ItemCount; - job.Progress = pct; - } - else - { - job.Progress = null; - } - - if (jobItems.Any(i => i.Status == SyncJobItemStatus.Transferring)) - { - job.Status = SyncJobStatus.Transferring; - } - else if (jobItems.Any(i => i.Status == SyncJobItemStatus.Converting)) - { - job.Status = SyncJobStatus.Converting; - } - else if (jobItems.All(i => i.Status == SyncJobItemStatus.Failed)) - { - job.Status = SyncJobStatus.Failed; - } - else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled)) - { - job.Status = SyncJobStatus.Cancelled; - } - else if (jobItems.All(i => i.Status == SyncJobItemStatus.ReadyToTransfer)) - { - job.Status = SyncJobStatus.ReadyToTransfer; - } - else if (jobItems.All(i => i.Status == SyncJobItemStatus.Cancelled || i.Status == SyncJobItemStatus.Failed || i.Status == SyncJobItemStatus.Synced || i.Status == SyncJobItemStatus.RemovedFromDevice)) - { - if (jobItems.Any(i => i.Status == SyncJobItemStatus.Failed)) - { - job.Status = SyncJobStatus.CompletedWithError; - } - else - { - job.Status = SyncJobStatus.Completed; - } - } - else - { - job.Status = SyncJobStatus.Queued; - } - - await _syncRepo.Update(job).ConfigureAwait(false); - - _syncManager.OnSyncJobUpdated(job); - } - - public async Task> GetItemsForSync(SyncCategory? category, string parentId, IEnumerable itemIds, User user, bool unwatchedOnly) - { - var list = new List(); - - if (category.HasValue) - { - list = (await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false)).ToList(); - } - else - { - foreach (var itemId in itemIds) - { - var subList = await GetItemsForSync(itemId, user).ConfigureAwait(false); - list.AddRange(subList); - } - } - - IEnumerable items = list; - items = items.Where(_syncManager.SupportsSync); - - if (unwatchedOnly) - { - // Avoid implicitly captured closure - var currentUser = user; - - items = items.Where(i => - { - var video = i as Video; - - if (video != null) - { - return !video.IsPlayed(currentUser); - } - - return true; - }); - } - - return items.DistinctBy(i => i.Id); - } - - private async Task> GetItemsForSync(SyncCategory category, string parentId, User user) - { - var parent = string.IsNullOrWhiteSpace(parentId) - ? user.RootFolder - : (Folder)_libraryManager.GetItemById(parentId); - - InternalItemsQuery query; - - switch (category) - { - case SyncCategory.Latest: - query = new InternalItemsQuery - { - IsFolder = false, - SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }, - SortOrder = SortOrder.Descending, - Recursive = true - }; - break; - case SyncCategory.Resume: - query = new InternalItemsQuery - { - IsFolder = false, - SortBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }, - SortOrder = SortOrder.Descending, - Recursive = true, - IsResumable = true, - MediaTypes = new[] { MediaType.Video } - }; - break; - - case SyncCategory.NextUp: - return _tvSeriesManager.GetNextUp(new NextUpQuery - { - ParentId = parentId, - UserId = user.Id.ToString("N") - }).Items; - - default: - throw new ArgumentException("Unrecognized category: " + category); - } - - if (parent == null) - { - return new List(); - } - - query.User = user; - - var result = await parent.GetItems(query).ConfigureAwait(false); - return result.Items; - } - - private async Task> GetItemsForSync(string id, User user) - { - var item = _libraryManager.GetItemById(id); - - if (item == null) - { - return new List(); - } - - var itemByName = item as IItemByName; - if (itemByName != null) - { - return itemByName.GetTaggedItems(new InternalItemsQuery(user) - { - IsFolder = false, - Recursive = true - }).ToList(); - } - - if (item.IsFolder) - { - var folder = (Folder)item; - var itemsResult = await folder.GetItems(new InternalItemsQuery(user) - { - Recursive = true, - IsFolder = false - - }).ConfigureAwait(false); - - var items = itemsResult.Items; - - if (!folder.IsPreSorted) - { - items = _libraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) - .ToArray(); - } - - return items.ToList(); - } - - return new List { item }; - } - - private async Task EnsureSyncJobItems(string targetId, CancellationToken cancellationToken) - { - var jobResult = _syncRepo.GetJobs(new SyncJobQuery - { - SyncNewContent = true, - TargetId = targetId - }); - - foreach (var job in jobResult.Items) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (job.SyncNewContent) - { - await EnsureJobItems(job).ConfigureAwait(false); - } - } - } - - public async Task Sync(IProgress progress, CancellationToken cancellationToken) - { - await EnsureSyncJobItems(null, cancellationToken).ConfigureAwait(false); - - // Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow - await HandleDeletedSyncFiles(cancellationToken).ConfigureAwait(false); - - // If it already has a converting status then is must have been aborted during conversion - var result = _syncManager.GetJobItems(new SyncJobItemQuery - { - Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, - AddMetadata = false - }); - - await SyncJobItems(result.Items, true, progress, cancellationToken).ConfigureAwait(false); - - CleanDeadSyncFiles(); - } - - private async Task HandleDeletedSyncFiles(CancellationToken cancellationToken) - { - // Look job items that are supposedly transfering, but need to be requeued because the synced files have been deleted somehow - var result = _syncManager.GetJobItems(new SyncJobItemQuery - { - Statuses = new[] { SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Transferring }, - AddMetadata = false - }); - - foreach (var item in result.Items) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (string.IsNullOrWhiteSpace(item.OutputPath) || !_fileSystem.FileExists(item.OutputPath)) - { - item.Status = SyncJobItemStatus.Queued; - await _syncManager.UpdateSyncJobItemInternal(item).ConfigureAwait(false); - await UpdateJobStatus(item.JobId).ConfigureAwait(false); - } - } - } - - private void CleanDeadSyncFiles() - { - // TODO - // Clean files in sync temp folder that are not linked to any sync jobs - } - - public async Task SyncJobItems(string targetId, bool enableConversion, IProgress progress, - CancellationToken cancellationToken) - { - await EnsureSyncJobItems(targetId, cancellationToken).ConfigureAwait(false); - - // If it already has a converting status then is must have been aborted during conversion - var result = _syncManager.GetJobItems(new SyncJobItemQuery - { - Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, - TargetId = targetId, - AddMetadata = false - }); - - await SyncJobItems(result.Items, enableConversion, progress, cancellationToken).ConfigureAwait(false); - } - - public async Task SyncJobItems(SyncJobItem[] items, bool enableConversion, IProgress progress, CancellationToken cancellationToken) - { - if (items.Length > 0) - { - if (!SyncRegistrationInfo.Instance.IsRegistered) - { - _logger.Debug("Cancelling sync job processing. Please obtain a supporter membership."); - return; - } - } - - var numComplete = 0; - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - double percentPerItem = 1; - percentPerItem /= items.Length; - var startingPercent = numComplete * percentPerItem * 100; - - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(p => progress.Report(startingPercent + percentPerItem * p)); - - // Pull it fresh from the db just to make sure it wasn't deleted or cancelled while another item was converting - var jobItem = enableConversion ? _syncRepo.GetJobItem(item.Id) : item; - - if (jobItem != null) - { - if (jobItem.Status != SyncJobItemStatus.Cancelled) - { - await ProcessJobItem(jobItem, enableConversion, innerProgress, cancellationToken).ConfigureAwait(false); - } - - await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - - numComplete++; - double percent = numComplete; - percent /= items.Length; - progress.Report(100 * percent); - } - } - - private async Task ProcessJobItem(SyncJobItem jobItem, bool enableConversion, IProgress progress, CancellationToken cancellationToken) - { - if (jobItem == null) - { - throw new ArgumentNullException("jobItem"); - } - - var item = _libraryManager.GetItemById(jobItem.ItemId); - if (item == null) - { - jobItem.Status = SyncJobItemStatus.Failed; - _logger.Error("Unable to locate library item for JobItem {0}, ItemId {1}", jobItem.Id, jobItem.ItemId); - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - - jobItem.Progress = 0; - - var syncOptions = _config.GetSyncOptions(); - var job = _syncManager.GetJob(jobItem.JobId); - var user = _userManager.GetUserById(job.UserId); - if (user == null) - { - jobItem.Status = SyncJobItemStatus.Failed; - _logger.Error("User not found. Cannot complete the sync job."); - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - - // See if there's already another active job item for the same target - var existingJobItems = _syncManager.GetJobItems(new SyncJobItemQuery - { - AddMetadata = false, - ItemId = jobItem.ItemId, - TargetId = jobItem.TargetId, - Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring } - }); - - var duplicateJobItems = existingJobItems.Items - .Where(i => !string.Equals(i.Id, jobItem.Id, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (duplicateJobItems.Count > 0) - { - var syncProvider = _syncManager.GetSyncProvider(jobItem) as IHasDuplicateCheck; - - if (!duplicateJobItems.Any(i => AllowDuplicateJobItem(syncProvider, i, jobItem))) - { - _logger.Debug("Cancelling sync job item because there is already another active job for the same target."); - jobItem.Status = SyncJobItemStatus.Cancelled; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - } - - var video = item as Video; - if (video != null) - { - await Sync(jobItem, video, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); - } - - else if (item is Audio) - { - await Sync(jobItem, (Audio)item, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); - } - - else if (item is Photo) - { - await Sync(jobItem, (Photo)item, cancellationToken).ConfigureAwait(false); - } - - else - { - await SyncGeneric(jobItem, item, cancellationToken).ConfigureAwait(false); - } - } - - private bool AllowDuplicateJobItem(IHasDuplicateCheck provider, SyncJobItem original, SyncJobItem duplicate) - { - if (provider != null) - { - return provider.AllowDuplicateJobItem(original, duplicate); - } - - return true; - } - - private async Task Sync(SyncJobItem jobItem, Video item, User user, bool enableConversion, SyncOptions syncOptions, IProgress progress, CancellationToken cancellationToken) - { - var job = _syncManager.GetJob(jobItem.JobId); - var jobOptions = _syncManager.GetVideoOptions(jobItem, job); - var conversionOptions = new VideoOptions - { - Profile = jobOptions.DeviceProfile - }; - - conversionOptions.DeviceId = jobItem.TargetId; - conversionOptions.Context = EncodingContext.Static; - conversionOptions.ItemId = item.Id.ToString("N"); - conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); - - var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(conversionOptions); - var mediaSource = streamInfo.MediaSource; - - // No sense creating external subs if we're already burning one into the video - var externalSubs = streamInfo.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode ? - new List() : - streamInfo.GetExternalSubtitles(false, true, null, null); - - // Mark as requiring conversion if transcoding the video, or if any subtitles need to be extracted - var requiresVideoTranscoding = streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting; - var requiresConversion = requiresVideoTranscoding || externalSubs.Any(i => RequiresExtraction(i, mediaSource)); - - if (requiresConversion && !enableConversion) - { - return; - } - - jobItem.MediaSourceId = streamInfo.MediaSourceId; - jobItem.TemporaryPath = GetTemporaryPath(jobItem); - - if (requiresConversion) - { - jobItem.Status = SyncJobItemStatus.Converting; - } - - if (requiresVideoTranscoding) - { - // Save the job item now since conversion could take a while - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - - try - { - var lastJobUpdate = DateTime.MinValue; - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(async pct => - { - progress.Report(pct); - - if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds) - { - jobItem.Progress = pct / 2; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - }); - - jobItem.OutputPath = await _mediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, conversionOptions.Profile) - { - OutputDirectory = jobItem.TemporaryPath, - CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit, - ReadInputAtNativeFramerate = !syncOptions.EnableFullSpeedTranscoding - - }, innerProgress, cancellationToken); - - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - _syncManager.OnConversionComplete(jobItem); - } - catch (OperationCanceledException) - { - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - } - catch (Exception ex) - { - jobItem.Status = SyncJobItemStatus.Failed; - _logger.ErrorException("Error during sync transcoding", ex); - } - - if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued) - { - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - - jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, true).ConfigureAwait(false); - } - else - { - if (mediaSource.Protocol == MediaProtocol.File) - { - jobItem.OutputPath = mediaSource.Path; - } - else if (mediaSource.Protocol == MediaProtocol.Http) - { - jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false); - } - else - { - throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); - } - - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - jobItem.MediaSource = mediaSource; - } - - jobItem.MediaSource.SupportsTranscoding = false; - - if (externalSubs.Count > 0) - { - // Save the job item now since conversion could take a while - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - await ConvertSubtitles(jobItem, externalSubs, streamInfo, cancellationToken).ConfigureAwait(false); - } - - jobItem.Progress = 50; - jobItem.Status = SyncJobItemStatus.ReadyToTransfer; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - - private bool RequiresExtraction(SubtitleStreamInfo stream, MediaSourceInfo mediaSource) - { - var originalStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Subtitle && i.Index == stream.Index); - - return originalStream != null && !originalStream.IsExternal; - } - - private async Task ConvertSubtitles(SyncJobItem jobItem, - IEnumerable subtitles, - StreamInfo streamInfo, - CancellationToken cancellationToken) - { - var files = new List(); - - var mediaStreams = jobItem.MediaSource.MediaStreams - .Where(i => i.Type != MediaStreamType.Subtitle || !i.IsExternal) - .ToList(); - - var startingIndex = mediaStreams.Count == 0 ? - 0 : - mediaStreams.Select(i => i.Index).Max() + 1; - - foreach (var subtitle in subtitles) - { - var fileInfo = await ConvertSubtitles(jobItem.TemporaryPath, streamInfo, subtitle, cancellationToken).ConfigureAwait(false); - - // Reset this to a value that will be based on the output media - fileInfo.Index = startingIndex; - files.Add(fileInfo); - - mediaStreams.Add(new MediaStream - { - Index = startingIndex, - Codec = subtitle.Format, - IsForced = subtitle.IsForced, - IsExternal = true, - Language = subtitle.Language, - Path = fileInfo.Path, - SupportsExternalStream = true, - Type = MediaStreamType.Subtitle - }); - - startingIndex++; - } - - jobItem.AdditionalFiles.AddRange(files); - - jobItem.MediaSource.MediaStreams = mediaStreams; - } - - private async Task ConvertSubtitles(string temporaryPath, StreamInfo streamInfo, SubtitleStreamInfo subtitleStreamInfo, CancellationToken cancellationToken) - { - var subtitleStreamIndex = subtitleStreamInfo.Index; - - var filename = Guid.NewGuid() + "." + subtitleStreamInfo.Format.ToLower(); - - var path = Path.Combine(temporaryPath, filename); - - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - - using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, false, cancellationToken).ConfigureAwait(false)) - { - using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - - return new ItemFileInfo - { - Name = Path.GetFileName(path), - Path = path, - Type = ItemFileType.Subtitles, - Index = subtitleStreamIndex - }; - } - - private const int DatabaseProgressUpdateIntervalSeconds = 2; - - private async Task Sync(SyncJobItem jobItem, Audio item, User user, bool enableConversion, SyncOptions syncOptions, IProgress progress, CancellationToken cancellationToken) - { - var job = _syncManager.GetJob(jobItem.JobId); - var jobOptions = _syncManager.GetAudioOptions(jobItem, job); - var conversionOptions = new AudioOptions - { - Profile = jobOptions.DeviceProfile - }; - - conversionOptions.DeviceId = jobItem.TargetId; - conversionOptions.Context = EncodingContext.Static; - conversionOptions.ItemId = item.Id.ToString("N"); - conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); - - var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(conversionOptions); - var mediaSource = streamInfo.MediaSource; - - jobItem.MediaSourceId = streamInfo.MediaSourceId; - jobItem.TemporaryPath = GetTemporaryPath(jobItem); - - if (streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting) - { - if (!enableConversion) - { - return; - } - - jobItem.Status = SyncJobItemStatus.Converting; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - - try - { - var lastJobUpdate = DateTime.MinValue; - var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(async pct => - { - progress.Report(pct); - - if ((DateTime.UtcNow - lastJobUpdate).TotalSeconds >= DatabaseProgressUpdateIntervalSeconds) - { - jobItem.Progress = pct / 2; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - }); - - jobItem.OutputPath = await _mediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, conversionOptions.Profile) - { - OutputDirectory = jobItem.TemporaryPath, - CpuCoreLimit = syncOptions.TranscodingCpuCoreLimit - - }, innerProgress, cancellationToken); - - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - _syncManager.OnConversionComplete(jobItem); - } - catch (OperationCanceledException) - { - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - } - catch (Exception ex) - { - jobItem.Status = SyncJobItemStatus.Failed; - _logger.ErrorException("Error during sync transcoding", ex); - } - - if (jobItem.Status == SyncJobItemStatus.Failed || jobItem.Status == SyncJobItemStatus.Queued) - { - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - - jobItem.MediaSource = await GetEncodedMediaSource(jobItem.OutputPath, user, false).ConfigureAwait(false); - } - else - { - if (mediaSource.Protocol == MediaProtocol.File) - { - jobItem.OutputPath = mediaSource.Path; - } - else if (mediaSource.Protocol == MediaProtocol.Http) - { - jobItem.OutputPath = await DownloadFile(jobItem, mediaSource, cancellationToken).ConfigureAwait(false); - } - else - { - throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); - } - - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - jobItem.MediaSource = mediaSource; - } - - jobItem.MediaSource.SupportsTranscoding = false; - - jobItem.Progress = 50; - jobItem.Status = SyncJobItemStatus.ReadyToTransfer; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - - private async Task Sync(SyncJobItem jobItem, Photo item, CancellationToken cancellationToken) - { - jobItem.OutputPath = item.Path; - - jobItem.Progress = 50; - jobItem.Status = SyncJobItemStatus.ReadyToTransfer; - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - - private async Task SyncGeneric(SyncJobItem jobItem, BaseItem item, CancellationToken cancellationToken) - { - jobItem.OutputPath = item.Path; - - jobItem.Progress = 50; - jobItem.Status = SyncJobItemStatus.ReadyToTransfer; - jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - - private async Task DownloadFile(SyncJobItem jobItem, MediaSourceInfo mediaSource, CancellationToken cancellationToken) - { - // TODO: Download - return mediaSource.Path; - } - - public string GetTemporaryPath(SyncJob job) - { - return GetTemporaryPath(job.Id); - } - - public string GetTemporaryPath(string jobId) - { - var basePath = _config.GetSyncOptions().TemporaryPath; - - if (string.IsNullOrWhiteSpace(basePath)) - { - basePath = Path.Combine(_config.CommonApplicationPaths.ProgramDataPath, "sync"); - } - - return Path.Combine(basePath, jobId); - } - - public string GetTemporaryPath(SyncJobItem jobItem) - { - return Path.Combine(GetTemporaryPath(jobItem.JobId), jobItem.Id); - } - - private async Task GetEncodedMediaSource(string path, User user, bool isVideo) - { - var item = _libraryManager.ResolvePath(_fileSystem.GetFileSystemInfo(path)); - - await item.RefreshMetadata(CancellationToken.None).ConfigureAwait(false); - - var hasMediaSources = item as IHasMediaSources; - - var mediaSources = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false).ToList(); - - var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) - ? new string[] { } - : new[] { user.Configuration.AudioLanguagePreference }; - - var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) - ? new List() : new List { user.Configuration.SubtitleLanguagePreference }; - - foreach (var source in mediaSources) - { - if (isVideo) - { - source.DefaultAudioStreamIndex = - MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); - - var defaultAudioIndex = source.DefaultAudioStreamIndex; - var audioLangage = defaultAudioIndex == null - ? null - : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); - - source.DefaultAudioStreamIndex = - MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, preferredSubs, user.Configuration.SubtitleMode, audioLangage); - } - else - { - var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - - if (audio != null) - { - source.DefaultAudioStreamIndex = audio.Index; - } - - } - } - - return mediaSources.FirstOrDefault(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs deleted file mode 100644 index 7bcb7b05e..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ /dev/null @@ -1,1361 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Sync; -using MediaBrowser.Model.Users; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Tasks; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncManager : ISyncManager - { - private readonly ILibraryManager _libraryManager; - private readonly ISyncRepository _repo; - private readonly IImageProcessor _imageProcessor; - private readonly ILogger _logger; - private readonly IUserManager _userManager; - private readonly Func _dtoService; - private readonly IServerApplicationHost _appHost; - private readonly ITVSeriesManager _tvSeriesManager; - private readonly Func _mediaEncoder; - private readonly IFileSystem _fileSystem; - private readonly Func _subtitleEncoder; - private readonly IConfigurationManager _config; - private readonly IUserDataManager _userDataManager; - private readonly Func _mediaSourceManager; - private readonly IJsonSerializer _json; - private readonly ITaskManager _taskManager; - private readonly IMemoryStreamProvider _memoryStreamProvider; - - private ISyncProvider[] _providers = { }; - - public event EventHandler> SyncJobCreated; - public event EventHandler> SyncJobCancelled; - public event EventHandler> SyncJobUpdated; - public event EventHandler> SyncJobItemUpdated; - public event EventHandler> SyncJobItemCreated; - - public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func mediaEncoder, IFileSystem fileSystem, Func subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func mediaSourceManager, IJsonSerializer json, ITaskManager taskManager, IMemoryStreamProvider memoryStreamProvider) - { - _libraryManager = libraryManager; - _repo = repo; - _imageProcessor = imageProcessor; - _logger = logger; - _userManager = userManager; - _dtoService = dtoService; - _appHost = appHost; - _tvSeriesManager = tvSeriesManager; - _mediaEncoder = mediaEncoder; - _fileSystem = fileSystem; - _subtitleEncoder = subtitleEncoder; - _config = config; - _userDataManager = userDataManager; - _mediaSourceManager = mediaSourceManager; - _json = json; - _taskManager = taskManager; - _memoryStreamProvider = memoryStreamProvider; - } - - public void AddParts(IEnumerable providers) - { - _providers = providers.ToArray(); - } - - public IEnumerable ServerSyncProviders - { - get { return _providers.OfType(); } - } - - private readonly ConcurrentDictionary _dataProviders = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - public ISyncDataProvider GetDataProvider(IServerSyncProvider provider, SyncTarget target) - { - return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths, _memoryStreamProvider)); - } - - public async Task CreateJob(SyncJobRequest request) - { - var processor = GetSyncJobProcessor(); - - var user = _userManager.GetUserById(request.UserId); - - var items = (await processor - .GetItemsForSync(request.Category, request.ParentId, request.ItemIds, user, request.UnwatchedOnly).ConfigureAwait(false)) - .ToList(); - - if (items.Any(i => !SupportsSync(i))) - { - throw new ArgumentException("Item does not support sync."); - } - - if (string.IsNullOrWhiteSpace(request.Name)) - { - if (request.ItemIds.Count == 1) - { - request.Name = GetDefaultName(_libraryManager.GetItemById(request.ItemIds[0])); - } - } - - if (string.IsNullOrWhiteSpace(request.Name)) - { - request.Name = DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToShortTimeString(); - } - - var target = GetSyncTargets(request.UserId) - .FirstOrDefault(i => string.Equals(request.TargetId, i.Id)); - - if (target == null) - { - throw new ArgumentException("Sync target not found."); - } - - var jobId = Guid.NewGuid().ToString("N"); - - if (string.IsNullOrWhiteSpace(request.Quality)) - { - request.Quality = GetQualityOptions(request.TargetId) - .Where(i => i.IsDefault) - .Select(i => i.Id) - .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); - } - - var job = new SyncJob - { - Id = jobId, - Name = request.Name, - TargetId = target.Id, - UserId = request.UserId, - UnwatchedOnly = request.UnwatchedOnly, - ItemLimit = request.ItemLimit, - RequestedItemIds = request.ItemIds ?? new List(), - DateCreated = DateTime.UtcNow, - DateLastModified = DateTime.UtcNow, - SyncNewContent = request.SyncNewContent, - ItemCount = items.Count, - Category = request.Category, - ParentId = request.ParentId, - Quality = request.Quality, - Profile = request.Profile, - Bitrate = request.Bitrate - }; - - if (!request.Category.HasValue && request.ItemIds != null) - { - var requestedItems = request.ItemIds - .Select(_libraryManager.GetItemById) - .Where(i => i != null); - - // It's just a static list - if (!requestedItems.Any(i => i.IsFolder || i is IItemByName)) - { - job.SyncNewContent = false; - } - } - - await _repo.Create(job).ConfigureAwait(false); - - await processor.EnsureJobItems(job).ConfigureAwait(false); - - // If it already has a converting status then is must have been aborted during conversion - var jobItemsResult = GetJobItems(new SyncJobItemQuery - { - Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, - JobId = jobId, - AddMetadata = false - }); - - await processor.SyncJobItems(jobItemsResult.Items, false, new Progress(), CancellationToken.None) - .ConfigureAwait(false); - - jobItemsResult = GetJobItems(new SyncJobItemQuery - { - Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, - JobId = jobId, - AddMetadata = false - }); - - var returnResult = new SyncJobCreationResult - { - Job = GetJob(jobId), - JobItems = jobItemsResult.Items.ToList() - }; - - if (SyncJobCreated != null) - { - EventHelper.FireEventIfNotNull(SyncJobCreated, this, new GenericEventArgs - { - Argument = returnResult - - }, _logger); - } - - if (returnResult.JobItems.Any(i => i.Status == SyncJobItemStatus.Queued || i.Status == SyncJobItemStatus.Converting)) - { - _taskManager.QueueScheduledTask(); - } - - return returnResult; - } - - public async Task UpdateJob(SyncJob job) - { - // Get fresh from the db and only update the fields that are supported to be changed. - var instance = _repo.GetJob(job.Id); - - instance.Name = job.Name; - instance.Quality = job.Quality; - instance.Profile = job.Profile; - instance.UnwatchedOnly = job.UnwatchedOnly; - instance.SyncNewContent = job.SyncNewContent; - instance.ItemLimit = job.ItemLimit; - - await _repo.Update(instance).ConfigureAwait(false); - - OnSyncJobUpdated(instance); - } - - internal void OnSyncJobUpdated(SyncJob job) - { - if (SyncJobUpdated != null) - { - EventHelper.FireEventIfNotNull(SyncJobUpdated, this, new GenericEventArgs - { - Argument = job - - }, _logger); - } - } - - internal async Task UpdateSyncJobItemInternal(SyncJobItem jobItem) - { - await _repo.Update(jobItem).ConfigureAwait(false); - - if (SyncJobUpdated != null) - { - EventHelper.FireEventIfNotNull(SyncJobItemUpdated, this, new GenericEventArgs - { - Argument = jobItem - - }, _logger); - } - } - - internal void OnSyncJobItemCreated(SyncJobItem job) - { - if (SyncJobUpdated != null) - { - EventHelper.FireEventIfNotNull(SyncJobItemCreated, this, new GenericEventArgs - { - Argument = job - - }, _logger); - } - } - - public async Task> GetJobs(SyncJobQuery query) - { - var result = _repo.GetJobs(query); - - foreach (var item in result.Items) - { - await FillMetadata(item).ConfigureAwait(false); - } - - return result; - } - - private async Task FillMetadata(SyncJob job) - { - var user = _userManager.GetUserById(job.UserId); - - if (user == null) - { - return; - } - - var target = GetSyncTargets(job.UserId) - .FirstOrDefault(i => string.Equals(i.Id, job.TargetId, StringComparison.OrdinalIgnoreCase)); - - if (target != null) - { - job.TargetName = target.Name; - } - - var item = job.RequestedItemIds - .Select(_libraryManager.GetItemById) - .FirstOrDefault(i => i != null); - - if (item == null) - { - var processor = GetSyncJobProcessor(); - - item = (await processor - .GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false)) - .FirstOrDefault(); - } - - if (item != null) - { - var hasSeries = item as IHasSeries; - if (hasSeries != null) - { - job.ParentName = hasSeries.SeriesName; - } - - var hasAlbumArtist = item as IHasAlbumArtist; - if (hasAlbumArtist != null) - { - job.ParentName = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - } - - var primaryImage = item.GetImageInfo(ImageType.Primary, 0); - var itemWithImage = item; - - if (primaryImage == null) - { - var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); - - if (parentWithImage != null) - { - itemWithImage = parentWithImage; - primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0); - } - } - - if (primaryImage != null) - { - try - { - job.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary); - job.PrimaryImageItemId = itemWithImage.Id.ToString("N"); - - } - catch (Exception ex) - { - _logger.ErrorException("Error getting image info", ex); - } - } - } - } - - private void FillMetadata(SyncJobItem jobItem) - { - var item = _libraryManager.GetItemById(jobItem.ItemId); - - if (item == null) - { - return; - } - - var primaryImage = item.GetImageInfo(ImageType.Primary, 0); - var itemWithImage = item; - - if (primaryImage == null) - { - var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); - - if (parentWithImage != null) - { - itemWithImage = parentWithImage; - primaryImage = parentWithImage.GetImageInfo(ImageType.Primary, 0); - } - } - - if (primaryImage != null) - { - try - { - jobItem.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary); - jobItem.PrimaryImageItemId = itemWithImage.Id.ToString("N"); - - } - catch (Exception ex) - { - _logger.ErrorException("Error getting image info", ex); - } - } - } - - public async Task CancelJob(string id) - { - var job = GetJob(id); - - if (job == null) - { - throw new ArgumentException("Job not found."); - } - - await _repo.DeleteJob(id).ConfigureAwait(false); - - var path = GetSyncJobProcessor().GetTemporaryPath(id); - - try - { - _fileSystem.DeleteDirectory(path, true); - } - catch (DirectoryNotFoundException) - { - - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting directory {0}", ex, path); - } - - if (SyncJobCancelled != null) - { - EventHelper.FireEventIfNotNull(SyncJobCancelled, this, new GenericEventArgs - { - Argument = job - - }, _logger); - } - } - - public SyncJob GetJob(string id) - { - return _repo.GetJob(id); - } - - public IEnumerable GetSyncTargets(string userId) - { - return _providers - .SelectMany(i => GetSyncTargets(i, userId)) - .OrderBy(i => i.Name); - } - - private IEnumerable GetSyncTargets(ISyncProvider provider) - { - return provider.GetAllSyncTargets().Select(i => new SyncTarget - { - Name = i.Name, - Id = GetSyncTargetId(provider, i) - }); - } - - private IEnumerable GetSyncTargets(ISyncProvider provider, string userId) - { - return provider.GetSyncTargets(userId).Select(i => new SyncTarget - { - Name = i.Name, - Id = GetSyncTargetId(provider, i) - }); - } - - private string GetSyncTargetId(ISyncProvider provider, SyncTarget target) - { - var hasUniqueId = provider as IHasUniqueTargetIds; - - if (hasUniqueId != null) - { - return target.Id; - } - - return target.Id; - //var providerId = GetSyncProviderId(provider); - //return (providerId + "-" + target.Id).GetMD5().ToString("N"); - } - - private string GetSyncProviderId(ISyncProvider provider) - { - return provider.GetType().Name.GetMD5().ToString("N"); - } - - public bool SupportsSync(BaseItem item) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (item is Playlist) - { - return true; - } - - if (item is Person) - { - return false; - } - - if (item is Year) - { - return false; - } - - if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) || - string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) || - string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase) || - string.Equals(item.MediaType, MediaType.Game, StringComparison.OrdinalIgnoreCase) || - string.Equals(item.MediaType, MediaType.Book, StringComparison.OrdinalIgnoreCase)) - { - if (item.LocationType == LocationType.Virtual) - { - return false; - } - - var video = item as Video; - if (video != null) - { - if (video.IsPlaceHolder) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - } - - if (item.SourceType != SourceType.Library) - { - return false; - } - - return true; - } - - if (item.SourceType == SourceType.Channel) - { - return BaseItem.ChannelManager.SupportsSync(item.ChannelId); - } - - return item.LocationType == LocationType.FileSystem || item is Season; - } - - private string GetDefaultName(BaseItem item) - { - return item.Name; - } - - public async Task ReportSyncJobItemTransferred(string id) - { - var jobItem = _repo.GetJobItem(id); - - jobItem.Status = SyncJobItemStatus.Synced; - jobItem.Progress = 100; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(jobItem.TemporaryPath)) - { - try - { - _fileSystem.DeleteDirectory(jobItem.TemporaryPath, true); - } - catch (DirectoryNotFoundException) - { - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting temporary job file: {0}", ex, jobItem.OutputPath); - } - } - } - - private SyncJobProcessor GetSyncJobProcessor() - { - return new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, _mediaEncoder(), _subtitleEncoder(), _config, _fileSystem, _mediaSourceManager()); - } - - public SyncJobItem GetJobItem(string id) - { - return _repo.GetJobItem(id); - } - - public QueryResult GetJobItems(SyncJobItemQuery query) - { - var result = _repo.GetJobItems(query); - - if (query.AddMetadata) - { - foreach (var item in result.Items) - { - FillMetadata(item); - } - } - - return result; - } - - private SyncedItem GetJobItemInfo(SyncJobItem jobItem) - { - var job = _repo.GetJob(jobItem.JobId); - - if (job == null) - { - _logger.Error("GetJobItemInfo job id {0} no longer exists", jobItem.JobId); - return null; - } - - var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); - - if (libraryItem == null) - { - _logger.Error("GetJobItemInfo library item with id {0} no longer exists", jobItem.ItemId); - return null; - } - - var syncedItem = new SyncedItem - { - SyncJobId = jobItem.JobId, - SyncJobItemId = jobItem.Id, - ServerId = _appHost.SystemId, - UserId = job.UserId, - SyncJobName = job.Name, - SyncJobDateCreated = job.DateCreated, - AdditionalFiles = jobItem.AdditionalFiles.Select(i => new ItemFileInfo - { - ImageType = i.ImageType, - Name = i.Name, - Type = i.Type, - Index = i.Index - - }).ToList() - }; - - var dtoOptions = new DtoOptions(); - - // Remove some bloat - dtoOptions.Fields.Remove(ItemFields.MediaStreams); - dtoOptions.Fields.Remove(ItemFields.IndexOptions); - dtoOptions.Fields.Remove(ItemFields.MediaSourceCount); - dtoOptions.Fields.Remove(ItemFields.Path); - dtoOptions.Fields.Remove(ItemFields.SeriesGenres); - dtoOptions.Fields.Remove(ItemFields.Settings); - dtoOptions.Fields.Remove(ItemFields.SyncInfo); - dtoOptions.Fields.Remove(ItemFields.BasicSyncInfo); - - syncedItem.Item = _dtoService().GetBaseItemDto(libraryItem, dtoOptions); - - var mediaSource = jobItem.MediaSource; - - syncedItem.Item.MediaSources = new List(); - - syncedItem.OriginalFileName = Path.GetFileName(libraryItem.Path); - if (string.IsNullOrWhiteSpace(syncedItem.OriginalFileName)) - { - syncedItem.OriginalFileName = Path.GetFileName(mediaSource.Path); - } - - // This will be null for items that are not audio/video - if (mediaSource != null) - { - syncedItem.OriginalFileName = Path.ChangeExtension(syncedItem.OriginalFileName, Path.GetExtension(mediaSource.Path)); - syncedItem.Item.MediaSources.Add(mediaSource); - } - if (string.IsNullOrWhiteSpace(syncedItem.OriginalFileName)) - { - syncedItem.OriginalFileName = libraryItem.Name; - } - - return syncedItem; - } - - public Task ReportOfflineAction(UserAction action) - { - switch (action.Type) - { - case UserActionType.PlayedItem: - return ReportOfflinePlayedItem(action); - default: - throw new ArgumentException("Unexpected action type"); - } - } - - private Task ReportOfflinePlayedItem(UserAction action) - { - var item = _libraryManager.GetItemById(action.ItemId); - var userData = _userDataManager.GetUserData(action.UserId, item); - - userData.LastPlayedDate = action.Date; - _userDataManager.UpdatePlayState(item, userData, action.PositionTicks); - - return _userDataManager.SaveUserData(new Guid(action.UserId), item, userData, UserDataSaveReason.Import, CancellationToken.None); - } - - public async Task> GetReadySyncItems(string targetId) - { - var processor = GetSyncJobProcessor(); - - await processor.SyncJobItems(targetId, false, new Progress(), CancellationToken.None).ConfigureAwait(false); - - var jobItemResult = GetJobItems(new SyncJobItemQuery - { - TargetId = targetId, - Statuses = new[] - { - SyncJobItemStatus.ReadyToTransfer, - SyncJobItemStatus.Transferring - } - }); - - var readyItems = jobItemResult.Items - .Select(GetJobItemInfo) - .Where(i => i != null) - .ToList(); - - _logger.Debug("Returning {0} ready sync items for targetId {1}", readyItems.Count, targetId); - - return readyItems; - } - - public async Task SyncData(SyncDataRequest request) - { - if (request.SyncJobItemIds != null) - { - return await SyncDataUsingSyncJobItemIds(request).ConfigureAwait(false); - } - - var jobItemResult = GetJobItems(new SyncJobItemQuery - { - TargetId = request.TargetId, - Statuses = new[] { SyncJobItemStatus.Synced } - }); - - var response = new SyncDataResponse(); - - foreach (var jobItem in jobItemResult.Items) - { - var requiresSaving = false; - var removeFromDevice = false; - - if (request.LocalItemIds.Contains(jobItem.ItemId, StringComparer.OrdinalIgnoreCase)) - { - var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); - - var job = _repo.GetJob(jobItem.JobId); - var user = _userManager.GetUserById(job.UserId); - - if (jobItem.IsMarkedForRemoval) - { - // Tell the device to remove it since it has been marked for removal - _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.ItemId); - removeFromDevice = true; - } - else if (user == null) - { - // Tell the device to remove it since the user is gone now - _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.ItemId); - removeFromDevice = true; - } - else if (!IsLibraryItemAvailable(libraryItem)) - { - // Tell the device to remove it since it's no longer available - _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.ItemId); - removeFromDevice = true; - } - else if (job.UnwatchedOnly) - { - if (libraryItem is Video && libraryItem.IsPlayed(user)) - { - // Tell the device to remove it since it has been played - _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.ItemId); - removeFromDevice = true; - } - } - else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) - { - _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - requiresSaving = true; - } - } - else - { - // Content is no longer on the device - if (jobItem.IsMarkedForRemoval) - { - jobItem.Status = SyncJobItemStatus.RemovedFromDevice; - } - else - { - _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.ItemId); - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - } - requiresSaving = true; - } - - if (removeFromDevice) - { - response.ItemIdsToRemove.Add(jobItem.ItemId); - jobItem.IsMarkedForRemoval = true; - requiresSaving = true; - } - - if (requiresSaving) - { - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - } - - // Now check each item that's on the device - foreach (var itemId in request.LocalItemIds) - { - // See if it's already marked for removal - if (response.ItemIdsToRemove.Contains(itemId, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - // If there isn't a sync job for this item, mark it for removal - if (!jobItemResult.Items.Any(i => string.Equals(itemId, i.ItemId, StringComparison.OrdinalIgnoreCase))) - { - response.ItemIdsToRemove.Add(itemId); - } - } - - response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - - var itemsOnDevice = request.LocalItemIds - .Except(response.ItemIdsToRemove) - .ToList(); - - SetUserAccess(request, response, itemsOnDevice); - - return response; - } - - private async Task SyncDataUsingSyncJobItemIds(SyncDataRequest request) - { - var jobItemResult = GetJobItems(new SyncJobItemQuery - { - TargetId = request.TargetId, - Statuses = new[] { SyncJobItemStatus.Synced } - }); - - var response = new SyncDataResponse(); - - foreach (var jobItem in jobItemResult.Items) - { - var requiresSaving = false; - var removeFromDevice = false; - - if (request.SyncJobItemIds.Contains(jobItem.Id, StringComparer.OrdinalIgnoreCase)) - { - var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); - - var job = _repo.GetJob(jobItem.JobId); - var user = _userManager.GetUserById(job.UserId); - - if (jobItem.IsMarkedForRemoval) - { - // Tell the device to remove it since it has been marked for removal - _logger.Info("Adding ItemIdsToRemove {0} because IsMarkedForRemoval is set.", jobItem.Id); - removeFromDevice = true; - } - else if (user == null) - { - // Tell the device to remove it since the user is gone now - _logger.Info("Adding ItemIdsToRemove {0} because the user is no longer valid.", jobItem.Id); - removeFromDevice = true; - } - else if (!IsLibraryItemAvailable(libraryItem)) - { - // Tell the device to remove it since it's no longer available - _logger.Info("Adding ItemIdsToRemove {0} because it is no longer available.", jobItem.Id); - removeFromDevice = true; - } - else if (job.UnwatchedOnly) - { - if (libraryItem is Video && libraryItem.IsPlayed(user)) - { - // Tell the device to remove it since it has been played - _logger.Info("Adding ItemIdsToRemove {0} because it has been marked played.", jobItem.Id); - removeFromDevice = true; - } - } - else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) - { - _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - requiresSaving = true; - } - } - else - { - // Content is no longer on the device - if (jobItem.IsMarkedForRemoval) - { - jobItem.Status = SyncJobItemStatus.RemovedFromDevice; - } - else - { - _logger.Info("Setting status to Queued for {0} because it is no longer on the device.", jobItem.Id); - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - } - requiresSaving = true; - } - - if (removeFromDevice) - { - response.ItemIdsToRemove.Add(jobItem.Id); - jobItem.IsMarkedForRemoval = true; - requiresSaving = true; - } - - if (requiresSaving) - { - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - } - } - - // Now check each item that's on the device - foreach (var syncJobItemId in request.SyncJobItemIds) - { - // See if it's already marked for removal - if (response.ItemIdsToRemove.Contains(syncJobItemId, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - // If there isn't a sync job for this item, mark it for removal - if (!jobItemResult.Items.Any(i => string.Equals(syncJobItemId, i.Id, StringComparison.OrdinalIgnoreCase))) - { - response.ItemIdsToRemove.Add(syncJobItemId); - } - } - - response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); - - return response; - } - - private void SetUserAccess(SyncDataRequest request, SyncDataResponse response, List itemIds) - { - var users = request.OfflineUserIds - .Select(_userManager.GetUserById) - .Where(i => i != null) - .ToList(); - - foreach (var itemId in itemIds) - { - var item = _libraryManager.GetItemById(itemId); - - if (item != null) - { - response.ItemUserAccess[itemId] = users - .Where(i => IsUserVisible(item, i)) - .Select(i => i.Id.ToString("N")) - .OrderBy(i => i) - .ToList(); - } - } - } - - private bool IsUserVisible(BaseItem item, User user) - { - return item.IsVisibleStandalone(user); - } - - private bool IsLibraryItemAvailable(BaseItem item) - { - if (item == null) - { - return false; - } - - return true; - } - - public async Task ReEnableJobItem(string id) - { - var jobItem = _repo.GetJobItem(id); - - if (jobItem.Status != SyncJobItemStatus.Failed && jobItem.Status != SyncJobItemStatus.Cancelled) - { - throw new ArgumentException("Operation is not valid for this job item"); - } - - jobItem.Status = SyncJobItemStatus.Queued; - jobItem.Progress = 0; - jobItem.IsMarkedForRemoval = false; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - - public async Task CancelItems(string targetId, IEnumerable itemIds) - { - foreach (var item in itemIds) - { - var syncJobItemResult = GetJobItems(new SyncJobItemQuery - { - AddMetadata = false, - ItemId = item, - TargetId = targetId, - Statuses = new[] { SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Converting, SyncJobItemStatus.Synced, SyncJobItemStatus.Failed } - }); - - foreach (var jobItem in syncJobItemResult.Items) - { - await CancelJobItem(jobItem.Id).ConfigureAwait(false); - } - } - } - - public async Task CancelJobItem(string id) - { - var jobItem = _repo.GetJobItem(id); - - if (jobItem.Status != SyncJobItemStatus.Queued && jobItem.Status != SyncJobItemStatus.ReadyToTransfer && jobItem.Status != SyncJobItemStatus.Converting && jobItem.Status != SyncJobItemStatus.Failed && jobItem.Status != SyncJobItemStatus.Synced && jobItem.Status != SyncJobItemStatus.Transferring) - { - throw new ArgumentException("Operation is not valid for this job item"); - } - - if (jobItem.Status != SyncJobItemStatus.Synced) - { - jobItem.Status = SyncJobItemStatus.Cancelled; - } - - jobItem.Progress = 0; - jobItem.IsMarkedForRemoval = true; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - - var path = processor.GetTemporaryPath(jobItem); - - try - { - _fileSystem.DeleteDirectory(path, true); - } - catch (DirectoryNotFoundException) - { - - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting directory {0}", ex, path); - } - - //var jobItemsResult = GetJobItems(new SyncJobItemQuery - //{ - // AddMetadata = false, - // JobId = jobItem.JobId, - // Limit = 0, - // Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Failed, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring } - //}); - - //if (jobItemsResult.TotalRecordCount == 0) - //{ - // await CancelJob(jobItem.JobId).ConfigureAwait(false); - //} - } - - public Task MarkJobItemForRemoval(string id) - { - return CancelJobItem(id); - } - - public async Task UnmarkJobItemForRemoval(string id) - { - var jobItem = _repo.GetJobItem(id); - - if (jobItem.Status != SyncJobItemStatus.Synced) - { - throw new ArgumentException("Operation is not valid for this job item"); - } - - jobItem.IsMarkedForRemoval = false; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - - public async Task ReportSyncJobItemTransferBeginning(string id) - { - var jobItem = _repo.GetJobItem(id); - - jobItem.Status = SyncJobItemStatus.Transferring; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - - public async Task ReportSyncJobItemTransferFailed(string id) - { - var jobItem = _repo.GetJobItem(id); - - jobItem.Status = SyncJobItemStatus.ReadyToTransfer; - - await UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - - var processor = GetSyncJobProcessor(); - - await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); - } - - public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) - { - return _repo.GetSyncedItemProgresses(query); - } - - public SyncJobOptions GetAudioOptions(SyncJobItem jobItem, SyncJob job) - { - var options = GetSyncJobOptions(jobItem.TargetId, null, null); - - if (job.Bitrate.HasValue) - { - options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; - } - - return options; - } - - public ISyncProvider GetSyncProvider(SyncJobItem jobItem) - { - foreach (var provider in _providers) - { - foreach (var target in GetSyncTargets(provider)) - { - if (string.Equals(target.Id, jobItem.TargetId, StringComparison.OrdinalIgnoreCase)) - { - return provider; - } - } - } - return null; - } - - public SyncJobOptions GetVideoOptions(SyncJobItem jobItem, SyncJob job) - { - var options = GetSyncJobOptions(jobItem.TargetId, job.Profile, job.Quality); - - if (job.Bitrate.HasValue) - { - options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; - } - - return options; - } - - private SyncJobOptions GetSyncJobOptions(string targetId, string profile, string quality) - { - foreach (var provider in _providers) - { - foreach (var target in GetSyncTargets(provider)) - { - if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) - { - return GetSyncJobOptions(provider, target, profile, quality); - } - } - } - - return GetDefaultSyncJobOptions(profile, quality); - } - - private SyncJobOptions GetSyncJobOptions(ISyncProvider provider, SyncTarget target, string profile, string quality) - { - var hasProfile = provider as IHasSyncQuality; - - if (hasProfile != null) - { - return hasProfile.GetSyncJobOptions(target, profile, quality); - } - - return GetDefaultSyncJobOptions(profile, quality); - } - - private SyncJobOptions GetDefaultSyncJobOptions(string profile, string quality) - { - var supportsAc3 = string.Equals(profile, "general", StringComparison.OrdinalIgnoreCase); - - var deviceProfile = new CloudSyncProfile(supportsAc3, false); - deviceProfile.MaxStaticBitrate = SyncHelper.AdjustBitrate(deviceProfile.MaxStaticBitrate, quality); - - return new SyncJobOptions - { - DeviceProfile = deviceProfile, - IsConverting = IsConverting(profile, quality) - }; - } - - private bool IsConverting(string profile, string quality) - { - return !string.Equals(profile, "original", StringComparison.OrdinalIgnoreCase); - } - - public IEnumerable GetQualityOptions(string targetId) - { - return GetQualityOptions(targetId, null); - } - - public IEnumerable GetQualityOptions(string targetId, User user) - { - foreach (var provider in _providers) - { - foreach (var target in GetSyncTargets(provider)) - { - if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) - { - return GetQualityOptions(provider, target, user); - } - } - } - - return new List(); - } - - private IEnumerable GetQualityOptions(ISyncProvider provider, SyncTarget target, User user) - { - var hasQuality = provider as IHasSyncQuality; - if (hasQuality != null) - { - var options = hasQuality.GetQualityOptions(target); - - if (user != null && !user.Policy.EnableSyncTranscoding) - { - options = options.Where(i => i.IsOriginalQuality); - } - - return options; - } - - // Default options for providers that don't override - return new List - { - new SyncQualityOption - { - Name = "High", - Id = "high", - IsDefault = true - }, - new SyncQualityOption - { - Name = "Medium", - Id = "medium" - }, - new SyncQualityOption - { - Name = "Low", - Id = "low" - }, - new SyncQualityOption - { - Name = "Custom", - Id = "custom" - } - }; - } - - public IEnumerable GetProfileOptions(string targetId, User user) - { - foreach (var provider in _providers) - { - foreach (var target in GetSyncTargets(provider)) - { - if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) - { - return GetProfileOptions(provider, target, user); - } - } - } - - return new List(); - } - - public IEnumerable GetProfileOptions(string targetId) - { - return GetProfileOptions(targetId, null); - } - - private IEnumerable GetProfileOptions(ISyncProvider provider, SyncTarget target, User user) - { - var hasQuality = provider as IHasSyncQuality; - if (hasQuality != null) - { - return hasQuality.GetProfileOptions(target); - } - - var list = new List(); - - list.Add(new SyncProfileOption - { - Name = "Original", - Id = "Original", - Description = "Syncs original files as-is.", - EnableQualityOptions = false - }); - - if (user == null || user.Policy.EnableSyncTranscoding) - { - list.Add(new SyncProfileOption - { - Name = "Baseline", - Id = "baseline", - Description = "Designed for compatibility with all devices, including web browsers. Targets H264/AAC video and MP3 audio." - }); - - list.Add(new SyncProfileOption - { - Name = "General", - Id = "general", - Description = "Designed for compatibility with Chromecast, Roku, Smart TV's, and other similar devices. Targets H264/AAC/AC3 video and MP3 audio.", - IsDefault = true - }); - } - - return list; - } - - protected internal void OnConversionComplete(SyncJobItem item) - { - var syncProvider = GetSyncProvider(item); - if (syncProvider is AppSyncProvider) - { - return; - } - - _taskManager.QueueIfNotRunning(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncNotificationEntryPoint.cs b/MediaBrowser.Server.Implementations/Sync/SyncNotificationEntryPoint.cs deleted file mode 100644 index 7017b422e..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncNotificationEntryPoint.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Sync; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncNotificationEntryPoint : IServerEntryPoint - { - private readonly ISessionManager _sessionManager; - private readonly ISyncManager _syncManager; - - public SyncNotificationEntryPoint(ISyncManager syncManager, ISessionManager sessionManager) - { - _syncManager = syncManager; - _sessionManager = sessionManager; - } - - public void Run() - { - _syncManager.SyncJobItemUpdated += _syncManager_SyncJobItemUpdated; - } - - private async void _syncManager_SyncJobItemUpdated(object sender, GenericEventArgs e) - { - var item = e.Argument; - - if (item.Status == SyncJobItemStatus.ReadyToTransfer) - { - try - { - await _sessionManager.SendMessageToUserDeviceSessions(item.TargetId, "SyncJobItemReady", item, CancellationToken.None).ConfigureAwait(false); - } - catch - { - - } - } - } - - public void Dispose() - { - _syncManager.SyncJobItemUpdated -= _syncManager_SyncJobItemUpdated; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncRegistrationInfo.cs b/MediaBrowser.Server.Implementations/Sync/SyncRegistrationInfo.cs deleted file mode 100644 index 40b84b1c2..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncRegistrationInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MediaBrowser.Common.Security; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncRegistrationInfo : IRequiresRegistration - { - private readonly ISecurityManager _securityManager; - - public static SyncRegistrationInfo Instance; - - public SyncRegistrationInfo(ISecurityManager securityManager) - { - _securityManager = securityManager; - Instance = this; - } - - private bool _registered; - public bool IsRegistered - { - get { return _registered; } - } - - public async Task LoadRegistrationInfoAsync() - { - var info = await _securityManager.GetRegistrationStatus("sync").ConfigureAwait(false); - - _registered = info.IsValid; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs deleted file mode 100644 index e0553b1b1..000000000 --- a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs +++ /dev/null @@ -1,158 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class SyncedMediaSourceProvider : IMediaSourceProvider - { - private readonly SyncManager _syncManager; - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - - public SyncedMediaSourceProvider(ISyncManager syncManager, IServerApplicationHost appHost, ILogger logger) - { - _appHost = appHost; - _logger = logger; - _syncManager = (SyncManager)syncManager; - } - - public async Task> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) - { - var jobItemResult = _syncManager.GetJobItems(new SyncJobItemQuery - { - AddMetadata = false, - Statuses = new[] { SyncJobItemStatus.Synced }, - ItemId = item.Id.ToString("N") - }); - - var list = new List(); - - if (jobItemResult.Items.Length > 0) - { - var targets = _syncManager.ServerSyncProviders - .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple(i, t))) - .ToList(); - - var serverId = _appHost.SystemId; - - foreach (var jobItem in jobItemResult.Items) - { - var targetTuple = targets.FirstOrDefault(i => string.Equals(i.Item2.Id, jobItem.TargetId, StringComparison.OrdinalIgnoreCase)); - - if (targetTuple != null) - { - var syncTarget = targetTuple.Item2; - var syncProvider = targetTuple.Item1; - var dataProvider = _syncManager.GetDataProvider(targetTuple.Item1, syncTarget); - - var localItems = await dataProvider.GetItems(syncTarget, serverId, item.Id.ToString("N")).ConfigureAwait(false); - - foreach (var localItem in localItems) - { - foreach (var mediaSource in localItem.Item.MediaSources) - { - AddMediaSource(list, localItem, mediaSource, syncProvider, syncTarget); - } - } - } - } - } - - return list; - } - - private void AddMediaSource(List list, - LocalItem item, - MediaSourceInfo mediaSource, - IServerSyncProvider provider, - SyncTarget target) - { - SetStaticMediaSourceInfo(item, mediaSource); - - var requiresDynamicAccess = provider as IHasDynamicAccess; - - if (requiresDynamicAccess != null) - { - mediaSource.RequiresOpening = true; - - var keyList = new List(); - keyList.Add(provider.GetType().FullName.GetMD5().ToString("N")); - keyList.Add(target.Id.GetMD5().ToString("N")); - keyList.Add(item.Id); - mediaSource.OpenToken = string.Join(StreamIdDelimeterString, keyList.ToArray()); - } - - list.Add(mediaSource); - } - - // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const string StreamIdDelimeterString = "_"; - - public async Task> OpenMediaSource(string openToken, CancellationToken cancellationToken) - { - var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3); - - var provider = _syncManager.ServerSyncProviders - .FirstOrDefault(i => string.Equals(openKeys[0], i.GetType().FullName.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase)); - - var target = provider.GetAllSyncTargets() - .FirstOrDefault(i => string.Equals(openKeys[1], i.Id.GetMD5().ToString("N"), StringComparison.OrdinalIgnoreCase)); - - var dataProvider = _syncManager.GetDataProvider(provider, target); - var localItem = await dataProvider.Get(target, openKeys[2]).ConfigureAwait(false); - - var fileId = localItem.FileId; - if (string.IsNullOrWhiteSpace(fileId)) - { - } - - var requiresDynamicAccess = (IHasDynamicAccess)provider; - var dynamicInfo = await requiresDynamicAccess.GetSyncedFileInfo(fileId, target, cancellationToken).ConfigureAwait(false); - - var mediaSource = localItem.Item.MediaSources.First(); - mediaSource.LiveStreamId = Guid.NewGuid().ToString(); - SetStaticMediaSourceInfo(localItem, mediaSource); - - foreach (var stream in mediaSource.MediaStreams) - { - if (!string.IsNullOrWhiteSpace(stream.ExternalId)) - { - var dynamicStreamInfo = await requiresDynamicAccess.GetSyncedFileInfo(stream.ExternalId, target, cancellationToken).ConfigureAwait(false); - stream.Path = dynamicStreamInfo.Path; - } - } - - mediaSource.Path = dynamicInfo.Path; - mediaSource.Protocol = dynamicInfo.Protocol; - mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders; - - return new Tuple(mediaSource, null); - } - - private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource) - { - mediaSource.Id = item.Id; - mediaSource.SupportsTranscoding = false; - if (mediaSource.Protocol == Model.MediaInfo.MediaProtocol.File) - { - mediaSource.ETag = item.Id; - } - } - - public Task CloseMediaSource(string liveStreamId) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs deleted file mode 100644 index 03df0d4e6..000000000 --- a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs +++ /dev/null @@ -1,188 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Sync; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class TargetDataProvider : ISyncDataProvider - { - private readonly SyncTarget _target; - private readonly IServerSyncProvider _provider; - - private readonly SemaphoreSlim _dataLock = new SemaphoreSlim(1, 1); - private List _items; - - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private readonly IFileSystem _fileSystem; - private readonly IApplicationPaths _appPaths; - private readonly IServerApplicationHost _appHost; - private readonly IMemoryStreamProvider _memoryStreamProvider; - - public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths, IMemoryStreamProvider memoryStreamProvider) - { - _logger = logger; - _json = json; - _provider = provider; - _target = target; - _fileSystem = fileSystem; - _appPaths = appPaths; - _memoryStreamProvider = memoryStreamProvider; - _appHost = appHost; - } - - private string[] GetRemotePath() - { - var parts = new List - { - _appHost.FriendlyName, - "data.json" - }; - - parts = parts.Select(i => GetValidFilename(_provider, i)).ToList(); - - return parts.ToArray(); - } - - private string GetValidFilename(IServerSyncProvider provider, string filename) - { - // We can always add this method to the sync provider if it's really needed - return _fileSystem.GetValidFilename(filename); - } - - private async Task> RetrieveItems(CancellationToken cancellationToken) - { - _logger.Debug("Getting {0} from {1}", string.Join(MediaSync.PathSeparatorString, GetRemotePath().ToArray()), _provider.Name); - - var fileResult = await _provider.GetFiles(GetRemotePath().ToArray(), _target, cancellationToken).ConfigureAwait(false); - - if (fileResult.Items.Length > 0) - { - using (var stream = await _provider.GetFile(fileResult.Items[0].FullName, _target, new Progress(), cancellationToken)) - { - return _json.DeserializeFromStream>(stream); - } - } - - return new List(); - } - - private async Task EnsureData(CancellationToken cancellationToken) - { - if (_items == null) - { - _items = await RetrieveItems(cancellationToken).ConfigureAwait(false); - } - } - - private async Task SaveData(List items, CancellationToken cancellationToken) - { - using (var stream = _memoryStreamProvider.CreateNew()) - { - _json.SerializeToStream(items, stream); - - // Save to sync provider - stream.Position = 0; - var remotePath = GetRemotePath(); - _logger.Debug("Saving data.json to {0}. Remote path: {1}", _provider.Name, string.Join("/", remotePath)); - - await _provider.SendFile(stream, remotePath, _target, new Progress(), cancellationToken).ConfigureAwait(false); - } - } - - private async Task GetData(bool enableCache, Func, T> dataFactory) - { - if (!enableCache) - { - var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false); - var newCache = items.ToList(); - var result = dataFactory(items); - await UpdateCache(newCache).ConfigureAwait(false); - return result; - } - - await _dataLock.WaitAsync().ConfigureAwait(false); - - try - { - await EnsureData(CancellationToken.None).ConfigureAwait(false); - - return dataFactory(_items); - } - finally - { - _dataLock.Release(); - } - } - - private async Task UpdateData(Func, List> action) - { - var items = await RetrieveItems(CancellationToken.None).ConfigureAwait(false); - items = action(items); - await SaveData(items.ToList(), CancellationToken.None).ConfigureAwait(false); - - await UpdateCache(null).ConfigureAwait(false); - } - - private async Task UpdateCache(List list) - { - await _dataLock.WaitAsync().ConfigureAwait(false); - - try - { - _items = list; - } - finally - { - _dataLock.Release(); - } - } - - public Task> GetLocalItems(SyncTarget target, string serverId) - { - return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase)).ToList()); - } - - public Task AddOrUpdate(SyncTarget target, LocalItem item) - { - return UpdateData(items => - { - var list = items.Where(i => !string.Equals(i.Id, item.Id, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - list.Add(item); - - return list; - }); - } - - public Task Delete(SyncTarget target, string id) - { - return UpdateData(items => items.Where(i => !string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)).ToList()); - } - - public Task Get(SyncTarget target, string id) - { - return GetData(true, items => items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase))); - } - - public Task> GetItems(SyncTarget target, string serverId, string itemId) - { - return GetData(true, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)).ToList()); - } - - public Task> GetItemsBySyncJobItemId(SyncTarget target, string serverId, string syncJobItemId) - { - return GetData(false, items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.SyncJobItemId, syncJobItemId, StringComparison.OrdinalIgnoreCase)).ToList()); - } - } -} diff --git a/MediaBrowser.Server.Implementations/TextEncoding/TextEncoding.cs b/MediaBrowser.Server.Implementations/TextEncoding/TextEncoding.cs deleted file mode 100644 index 4c047b7d5..000000000 --- a/MediaBrowser.Server.Implementations/TextEncoding/TextEncoding.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Text; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.TextEncoding; - -namespace MediaBrowser.Server.Implementations.TextEncoding -{ - public class TextEncoding : IEncoding - { - private readonly IFileSystem _fileSystem; - - public TextEncoding(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } - - public byte[] GetASCIIBytes(string text) - { - return Encoding.ASCII.GetBytes(text); - } - - public string GetASCIIString(byte[] bytes, int startIndex, int length) - { - return Encoding.ASCII.GetString(bytes, 0, bytes.Length); - } - - public Encoding GetFileEncoding(string srcFile) - { - // *** Detect byte order mark if any - otherwise assume default - var buffer = new byte[5]; - - using (var file = _fileSystem.OpenRead(srcFile)) - { - file.Read(buffer, 0, 5); - } - - if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) - return Encoding.UTF8; - if (buffer[0] == 0xfe && buffer[1] == 0xff) - return Encoding.Unicode; - if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff) - return Encoding.UTF32; - if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76) - return Encoding.UTF7; - - // It's ok - anything aside from utf is ok since that's what we're looking for - return Encoding.Default; - } - } -} diff --git a/MediaBrowser.Server.Implementations/Xml/XmlReaderSettingsFactory.cs b/MediaBrowser.Server.Implementations/Xml/XmlReaderSettingsFactory.cs deleted file mode 100644 index ae7018ad4..000000000 --- a/MediaBrowser.Server.Implementations/Xml/XmlReaderSettingsFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Xml; -using MediaBrowser.Model.Xml; - -namespace MediaBrowser.Server.Implementations.Xml -{ - public class XmlReaderSettingsFactory : IXmlReaderSettingsFactory - { - public XmlReaderSettings Create(bool enableValidation) - { - var settings = new XmlReaderSettings(); - - if (!enableValidation) - { - settings.ValidationType = ValidationType.None; - } - - return settings; - } - } -} diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 861d59908..11d401670 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -83,7 +83,10 @@ using System.Threading; using System.Threading.Tasks; using Emby.Common.Implementations; using Emby.Common.Implementations.Networking; +using Emby.Common.Implementations.Reflection; +using Emby.Common.Implementations.TextEncoding; using Emby.Common.Implementations.Updates; +using Emby.Common.Implementations.Xml; using Emby.Photos; using MediaBrowser.Model.IO; using MediaBrowser.Api.Playback; @@ -110,6 +113,7 @@ using Emby.Server.Implementations.MediaEncoder; using Emby.Server.Implementations.Notifications; using Emby.Server.Implementations.Persistence; using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.Sync; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using MediaBrowser.Model.Activity; @@ -124,10 +128,7 @@ using MediaBrowser.Model.Social; using MediaBrowser.Model.TextEncoding; using MediaBrowser.Model.Xml; using MediaBrowser.Server.Implementations.Archiving; -using MediaBrowser.Server.Implementations.Reflection; using MediaBrowser.Server.Implementations.Serialization; -using MediaBrowser.Server.Implementations.TextEncoding; -using MediaBrowser.Server.Implementations.Xml; using OpenSubtitlesHandler; using ServiceStack; using StringExtensions = MediaBrowser.Controller.Extensions.StringExtensions; -- cgit v1.2.3