diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/Sync')
15 files changed, 1236 insertions, 188 deletions
diff --git a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs index d35ff8fc4..99d758233 100644 --- a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs @@ -3,12 +3,13 @@ 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 + public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds, IHasSyncQuality { private readonly IDeviceManager _deviceManager; @@ -31,16 +32,77 @@ namespace MediaBrowser.Server.Implementations.Sync }); } - public DeviceProfile GetDeviceProfile(SyncTarget target) + public DeviceProfile GetDeviceProfile(SyncTarget target, string profile, string quality) { var caps = _deviceManager.GetCapabilities(target.Id); - return caps == null || caps.DeviceProfile == null ? new DeviceProfile() : caps.DeviceProfile; + 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 "App Sync"; } } + + public IEnumerable<SyncTarget> GetAllSyncTargets() + { + return _deviceManager.GetDevices(new DeviceQuery + { + SupportsSync = true + + }).Items.Select(i => new SyncTarget + { + Id = i.Id, + Name = i.Name + }); + } + + public IEnumerable<SyncQualityOption> GetQualityOptions(SyncTarget target) + { + return new List<SyncQualityOption> + { + 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" + } + }; + } + + public IEnumerable<SyncProfileOption> GetProfileOptions(SyncTarget target) + { + return new List<SyncProfileOption>(); + } + + 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 + }; + } } } diff --git a/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs b/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs new file mode 100644 index 000000000..e13042538 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/CloudSyncProfile.cs @@ -0,0 +1,123 @@ +using MediaBrowser.Model.Dlna; + +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"; + } + + 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 ContainerProfile[] { }; + + CodecProfiles = new[] + { + new CodecProfile + { + Type = CodecType.Video, + Conditions = new [] + { + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.VideoBitDepth, + Value = "8", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.Height, + Value = "1080", + IsRequired = false + }, + new ProfileCondition + { + Condition = ProfileConditionType.LessThanEqual, + Property = ProfileConditionValue.RefFrames, + Value = "12", + IsRequired = false + } + } + } + }; + + SubtitleProfiles = new[] + { + new SubtitleProfile + { + Format = "srt", + 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/CloudSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs deleted file mode 100644 index 59713b138..000000000 --- a/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MediaBrowser.Common; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dlna; -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 CloudSyncProvider : IServerSyncProvider - { - private readonly ICloudSyncProvider[] _providers = {}; - - public CloudSyncProvider(IApplicationHost appHost) - { - _providers = appHost.GetExports<ICloudSyncProvider>().ToArray(); - } - - public IEnumerable<SyncTarget> GetSyncTargets(string userId) - { - return _providers.SelectMany(i => i.GetSyncTargets(userId)); - } - - public DeviceProfile GetDeviceProfile(SyncTarget target) - { - return new DeviceProfile(); - } - - public string Name - { - get { return "Cloud Sync"; } - } - - public Task<List<string>> GetServerItemIds(string serverId, SyncTarget target, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task DeleteItem(string serverId, string itemId, SyncTarget target, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task TransferItemFile(string serverId, string itemId, string[] pathParts, string name, ItemFileType fileType, SyncTarget target, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs b/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs new file mode 100644 index 000000000..e7eee0923 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/IHasSyncQuality.cs @@ -0,0 +1,31 @@ +using MediaBrowser.Model.Sync; +using System.Collections.Generic; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public interface IHasSyncQuality + { + /// <summary> + /// Gets the device profile. + /// </summary> + /// <param name="target">The target.</param> + /// <param name="profile">The profile.</param> + /// <param name="quality">The quality.</param> + /// <returns>DeviceProfile.</returns> + SyncJobOptions GetSyncJobOptions(SyncTarget target, string profile, string quality); + + /// <summary> + /// Gets the quality options. + /// </summary> + /// <param name="target">The target.</param> + /// <returns>IEnumerable<SyncQualityOption>.</returns> + IEnumerable<SyncQualityOption> GetQualityOptions(SyncTarget target); + + /// <summary> + /// Gets the profile options. + /// </summary> + /// <param name="target">The target.</param> + /// <returns>IEnumerable<SyncQualityOption>.</returns> + IEnumerable<SyncProfileOption> GetProfileOptions(SyncTarget target); + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs b/MediaBrowser.Server.Implementations/Sync/MediaSync.cs index 099e45a6e..c9ed4637a 100644 --- a/MediaBrowser.Server.Implementations/Sync/MediaSync.cs +++ b/MediaBrowser.Server.Implementations/Sync/MediaSync.cs @@ -1,9 +1,18 @@ -using MediaBrowser.Common.Progress; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Progress; using MediaBrowser.Controller; 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.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,26 +23,25 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly ISyncManager _syncManager; private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; - public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost) + public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem) { _logger = logger; _syncManager = syncManager; _appHost = appHost; + _fileSystem = fileSystem; } - public async Task Sync(IServerSyncProvider provider, + public async Task Sync(IServerSyncProvider provider, + ISyncDataProvider dataProvider, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken) { var serverId = _appHost.SystemId; - await SyncData(provider, serverId, target, cancellationToken).ConfigureAwait(false); - progress.Report(2); - - // Do the data sync twice so the server knows what was removed from the device - await SyncData(provider, serverId, target, cancellationToken).ConfigureAwait(false); + await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); progress.Report(3); var innerProgress = new ActionableProgress<double>(); @@ -43,16 +51,21 @@ namespace MediaBrowser.Server.Implementations.Sync totalProgress += 1; progress.Report(totalProgress); }); - await GetNewMedia(provider, target, serverId, innerProgress, cancellationToken); + await GetNewMedia(provider, dataProvider, target, serverId, 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 localIds = await provider.GetServerItemIds(serverId, target, cancellationToken).ConfigureAwait(false); + var localIds = await dataProvider.GetServerItemIds(target, serverId).ConfigureAwait(false); var result = await _syncManager.SyncData(new SyncDataRequest { @@ -67,23 +80,24 @@ namespace MediaBrowser.Server.Implementations.Sync { try { - await RemoveItem(provider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false); + await RemoveItem(provider, dataProvider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { - _logger.ErrorException("Error deleting item from sync target. Id: {0}", ex, itemIdToRemove); + _logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove); } } } private async Task GetNewMedia(IServerSyncProvider provider, + ISyncDataProvider dataProvider, SyncTarget target, string serverId, IProgress<double> progress, CancellationToken cancellationToken) { - var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false); - + var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false); + var numComplete = 0; double startingPercent = 0; double percentPerItem = 1; @@ -105,7 +119,7 @@ namespace MediaBrowser.Server.Implementations.Sync progress.Report(totalProgress); }); - await GetItem(provider, target, serverId, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); + await GetItem(provider, dataProvider, target, serverId, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); numComplete++; startingPercent = numComplete; @@ -116,6 +130,7 @@ namespace MediaBrowser.Server.Implementations.Sync } private async Task GetItem(IServerSyncProvider provider, + ISyncDataProvider dataProvider, SyncTarget target, string serverId, SyncedItem jobItem, @@ -128,6 +143,8 @@ namespace MediaBrowser.Server.Implementations.Sync var fileTransferProgress = new ActionableProgress<double>(); fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92)); + var localItem = CreateLocalItem(provider, jobItem.SyncJobId, jobItem.SyncJobItemId, target, libraryItem, serverId, jobItem.OriginalFileName); + await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id); var transferSuccess = false; @@ -135,8 +152,21 @@ namespace MediaBrowser.Server.Implementations.Sync try { - //await provider.TransferItemFile(serverId, libraryItem.Id, internalSyncJobItem.OutputPath, target, cancellationToken) - // .ConfigureAwait(false); + var sendFileResult = await SendFile(provider, internalSyncJobItem.OutputPath, localItem, target, 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.SupportsTranscoding = false; + } + } + + // Create db record + await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); progress.Report(92); @@ -162,13 +192,176 @@ namespace MediaBrowser.Server.Implementations.Sync } } - private Task RemoveItem(IServerSyncProvider provider, + private async Task RemoveItem(IServerSyncProvider provider, + ISyncDataProvider dataProvider, string serverId, string itemId, SyncTarget target, CancellationToken cancellationToken) { - return provider.DeleteItem(serverId, itemId, target, cancellationToken); + var localItems = await dataProvider.GetCachedItems(target, serverId, itemId); + + foreach (var localItem in localItems) + { + var files = await GetFiles(provider, localItem, target, cancellationToken); + + foreach (var file in files) + { + await provider.DeleteFile(file.Path, target, cancellationToken).ConfigureAwait(false); + } + + await dataProvider.Delete(target, localItem.Id).ConfigureAwait(false); + } + } + + private async Task<SendFileResult> SendFile(IServerSyncProvider provider, string inputPath, LocalItem item, SyncTarget target, CancellationToken cancellationToken) + { + using (var stream = _fileSystem.GetFileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read, true)) + { + return await provider.SendFile(stream, item.LocalPath, target, new Progress<double>(), 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, string syncJobId, string syncJobItemId, SyncTarget target, BaseItemDto libraryItem, string serverId, string originalFileName) + { + var path = GetDirectoryPath(provider, syncJobId, libraryItem, serverId); + path.Add(GetLocalFileName(provider, libraryItem, originalFileName)); + + var localPath = provider.GetFullPath(path, target); + + 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(syncJobItemId, libraryItem.Id) + }; + } + + private List<string> GetDirectoryPath(IServerSyncProvider provider, string syncJobId, BaseItemDto item, string serverId) + { + var parts = new List<string> + { + serverId, + syncJobId + }; + + 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); + } + + private async Task<List<ItemFileInfo>> GetFiles(IServerSyncProvider provider, LocalItem item, SyncTarget target, CancellationToken cancellationToken) + { + var path = item.LocalPath; + path = provider.GetParentDirectoryPath(path, target); + + var list = await provider.GetFileSystemEntries(path, target, cancellationToken).ConfigureAwait(false); + + var itemFiles = new List<ItemFileInfo>(); + + var name = Path.GetFileNameWithoutExtension(item.LocalPath); + + foreach (var file in list.Where(f => f.Name.Contains(name))) + { + var itemFile = new ItemFileInfo + { + Path = file.Path, + Name = file.Name + }; + + if (IsSubtitleFile(file.Name)) + { + itemFile.Type = ItemFileType.Subtitles; + } + + itemFiles.Add(itemFile); + } + + return itemFiles; + } + + private static readonly string[] SupportedSubtitleExtensions = { ".srt", ".vtt" }; + private bool IsSubtitleFile(string path) + { + var ext = Path.GetExtension(path) ?? string.Empty; + + return SupportedSubtitleExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs b/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs new file mode 100644 index 000000000..a8bc24c2a --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/MultiProviderSync.cs @@ -0,0 +1,71 @@ +using MediaBrowser.Common.IO; +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; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public class MultiProviderSync + { + private readonly SyncManager _syncManager; + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + + public MultiProviderSync(SyncManager syncManager, IServerApplicationHost appHost, ILogger logger, IFileSystem fileSystem) + { + _syncManager = syncManager; + _appHost = appHost; + _logger = logger; + _fileSystem = fileSystem; + } + + public async Task Sync(IEnumerable<IServerSyncProvider> providers, IProgress<double> progress, CancellationToken cancellationToken) + { + var targets = providers + .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple<IServerSyncProvider, SyncTarget>(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<double>(); + 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) + .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 new file mode 100644 index 000000000..148602bd4 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/ServerSyncScheduledTask.cs @@ -0,0 +1,81 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.ScheduledTasks; +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; + +namespace MediaBrowser.Server.Implementations.Sync +{ + class ServerSyncScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey + { + private readonly ISyncManager _syncManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationHost _appHost; + + public ServerSyncScheduledTask(ISyncManager syncManager, ILogger logger, IFileSystem fileSystem, IServerApplicationHost appHost) + { + _syncManager = syncManager; + _logger = logger; + _fileSystem = fileSystem; + _appHost = appHost; + } + + 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<double> progress) + { + return new MultiProviderSync((SyncManager)_syncManager, _appHost, _logger, _fileSystem) + .Sync(ServerSyncProviders, progress, cancellationToken); + } + + public IEnumerable<IServerSyncProvider> ServerSyncProviders + { + get { return ((SyncManager)_syncManager).ServerSyncProviders; } + } + + public IEnumerable<ITaskTrigger> GetDefaultTriggers() + { + return new ITaskTrigger[] + { + new IntervalTrigger { Interval = TimeSpan.FromHours(3) } + }; + } + + public bool IsHidden + { + get { return !IsEnabled; } + } + + public bool IsEnabled + { + get { return ServerSyncProviders.Any(); } + } + + public string Key + { + get { return "ServerSync"; } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs index ccc9508e8..913d50e9d 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncConvertScheduledTask.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Sync { - public class SyncScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey + public class SyncConvertScheduledTask : IScheduledTask, IConfigurableScheduledTask, IHasKey { private readonly ILibraryManager _libraryManager; private readonly ISyncRepository _syncRepo; @@ -25,8 +25,9 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly IMediaSourceManager _mediaSourceManager; - public SyncScheduledTask(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfigurationManager config, IFileSystem fileSystem) + 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; @@ -38,6 +39,7 @@ namespace MediaBrowser.Server.Implementations.Sync _subtitleEncoder = subtitleEncoder; _config = config; _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; } public string Name @@ -60,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Sync public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - return new SyncJobProcessor(_libraryManager, _syncRepo, (SyncManager)_syncManager, _logger, _userManager, _tvSeriesManager, _mediaEncoder, _subtitleEncoder, _config, _fileSystem) + return new SyncJobProcessor(_libraryManager, _syncRepo, (SyncManager)_syncManager, _logger, _userManager, _tvSeriesManager, _mediaEncoder, _subtitleEncoder, _config, _fileSystem, _mediaSourceManager) .Sync(progress, cancellationToken); } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs b/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs new file mode 100644 index 000000000..006284ee1 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/SyncHelper.cs @@ -0,0 +1,24 @@ +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 = Convert.ToInt32(profileBitrate.Value * .75); + } + else if (string.Equals(quality, "low", StringComparison.OrdinalIgnoreCase)) + { + profileBitrate = Convert.ToInt32(profileBitrate.Value*.5); + } + } + + return profileBitrate; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs new file mode 100644 index 000000000..cb8141c89 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobOptions.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Model.Dlna; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public class SyncJobOptions + { + /// <summary> + /// Gets or sets the conversion options. + /// </summary> + /// <value>The conversion options.</value> + public DeviceProfile DeviceProfile { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this instance is converting. + /// </summary> + /// <value><c>true</c> if this instance is converting; otherwise, <c>false</c>.</value> + public bool IsConverting { get; set; } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs index 72dc1bdb6..22c59610b 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs @@ -38,8 +38,9 @@ namespace MediaBrowser.Server.Implementations.Sync 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) + 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; @@ -51,6 +52,7 @@ namespace MediaBrowser.Server.Implementations.Sync _subtitleEncoder = subtitleEncoder; _config = config; _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; } public async Task EnsureJobItems(SyncJob job) @@ -362,7 +364,7 @@ namespace MediaBrowser.Server.Implementations.Sync // If it already has a converting status then is must have been aborted during conversion var result = _syncRepo.GetJobItems(new SyncJobItemQuery { - Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting } + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting } }); await SyncJobItems(result.Items, true, progress, cancellationToken).ConfigureAwait(false); @@ -384,7 +386,7 @@ namespace MediaBrowser.Server.Implementations.Sync // If it already has a converting status then is must have been aborted during conversion var result = _syncRepo.GetJobItems(new SyncJobItemQuery { - Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, TargetId = targetId }); @@ -448,15 +450,6 @@ namespace MediaBrowser.Server.Implementations.Sync return; } - var deviceProfile = _syncManager.GetDeviceProfile(jobItem.TargetId); - if (deviceProfile == null) - { - jobItem.Status = SyncJobItemStatus.Failed; - _logger.Error("Unable to locate SyncTarget for JobItem {0}, SyncTargetId {1}", jobItem.Id, jobItem.TargetId); - await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - return; - } - jobItem.Progress = 0; var user = _userManager.GetUserById(job.UserId); @@ -464,17 +457,17 @@ namespace MediaBrowser.Server.Implementations.Sync var video = item as Video; if (video != null) { - await Sync(jobItem, job, video, user, deviceProfile, enableConversion, progress, cancellationToken).ConfigureAwait(false); + await Sync(jobItem, job, video, user, enableConversion, progress, cancellationToken).ConfigureAwait(false); } else if (item is Audio) { - await Sync(jobItem, job, (Audio)item, user, deviceProfile, enableConversion, progress, cancellationToken).ConfigureAwait(false); + await Sync(jobItem, job, (Audio)item, user, enableConversion, progress, cancellationToken).ConfigureAwait(false); } else if (item is Photo) { - await Sync(jobItem, (Photo)item, deviceProfile, cancellationToken).ConfigureAwait(false); + await Sync(jobItem, (Photo)item, cancellationToken).ConfigureAwait(false); } else @@ -483,17 +476,20 @@ namespace MediaBrowser.Server.Implementations.Sync } } - private async Task Sync(SyncJobItem jobItem, SyncJob job, Video item, User user, DeviceProfile profile, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) + private async Task Sync(SyncJobItem jobItem, SyncJob job, Video item, User user, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) { - var options = _syncManager.GetVideoOptions(jobItem, job); + var jobOptions = _syncManager.GetVideoOptions(jobItem, job); + var conversionOptions = new VideoOptions + { + Profile = jobOptions.DeviceProfile + }; - options.DeviceId = jobItem.TargetId; - options.Context = EncodingContext.Static; - options.Profile = profile; - options.ItemId = item.Id.ToString("N"); - options.MediaSources = item.GetMediaSources(false, user).ToList(); + 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().BuildVideoItem(options); + var streamInfo = new StreamBuilder().BuildVideoItem(conversionOptions); var mediaSource = streamInfo.MediaSource; // No sense creating external subs if we're already burning one into the video @@ -502,7 +498,7 @@ namespace MediaBrowser.Server.Implementations.Sync streamInfo.GetExternalSubtitles(false); // Mark as requiring conversion if transcoding the video, or if any subtitles need to be extracted - var requiresVideoTranscoding = streamInfo.PlayMethod == PlayMethod.Transcode && job.Quality != SyncQuality.Original; + var requiresVideoTranscoding = streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting; var requiresConversion = requiresVideoTranscoding || externalSubs.Any(i => RequiresExtraction(i, mediaSource)); if (requiresConversion && !enableConversion) @@ -540,7 +536,7 @@ namespace MediaBrowser.Server.Implementations.Sync } }); - jobItem.OutputPath = await _mediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, profile) + jobItem.OutputPath = await _mediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, conversionOptions.Profile) { OutputDirectory = jobItem.TemporaryPath @@ -583,6 +579,8 @@ namespace MediaBrowser.Server.Implementations.Sync jobItem.MediaSource = mediaSource; } + jobItem.MediaSource.SupportsTranscoding = false; + if (externalSubs.Count > 0) { // Save the job item now since conversion could take a while @@ -674,23 +672,26 @@ namespace MediaBrowser.Server.Implementations.Sync private const int DatabaseProgressUpdateIntervalSeconds = 2; - private async Task Sync(SyncJobItem jobItem, SyncJob job, Audio item, User user, DeviceProfile profile, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) + private async Task Sync(SyncJobItem jobItem, SyncJob job, Audio item, User user, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) { - var options = _syncManager.GetAudioOptions(jobItem); + var jobOptions = _syncManager.GetAudioOptions(jobItem, job); + var conversionOptions = new AudioOptions + { + Profile = jobOptions.DeviceProfile + }; - options.DeviceId = jobItem.TargetId; - options.Context = EncodingContext.Static; - options.Profile = profile; - options.ItemId = item.Id.ToString("N"); - options.MediaSources = item.GetMediaSources(false, user).ToList(); + 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().BuildAudioItem(options); + var streamInfo = new StreamBuilder().BuildAudioItem(conversionOptions); var mediaSource = streamInfo.MediaSource; jobItem.MediaSourceId = streamInfo.MediaSourceId; jobItem.TemporaryPath = GetTemporaryPath(jobItem); - if (streamInfo.PlayMethod == PlayMethod.Transcode && job.Quality != SyncQuality.Original) + if (streamInfo.PlayMethod == PlayMethod.Transcode && jobOptions.IsConverting) { if (!enableConversion) { @@ -717,7 +718,7 @@ namespace MediaBrowser.Server.Implementations.Sync } }); - jobItem.OutputPath = await _mediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, profile) + jobItem.OutputPath = await _mediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, conversionOptions.Profile) { OutputDirectory = jobItem.TemporaryPath @@ -760,12 +761,14 @@ namespace MediaBrowser.Server.Implementations.Sync 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, DeviceProfile profile, CancellationToken cancellationToken) + private async Task Sync(SyncJobItem jobItem, Photo item, CancellationToken cancellationToken) { jobItem.OutputPath = item.Path; diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index a2fd92bf5..dc539b408 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -15,16 +15,17 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Sync; using MediaBrowser.Controller.TV; -using MediaBrowser.Model.Dlna; 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 MoreLinq; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -47,7 +48,9 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly IFileSystem _fileSystem; private readonly Func<ISubtitleEncoder> _subtitleEncoder; private readonly IConfigurationManager _config; - private IUserDataManager _userDataManager; + private readonly IUserDataManager _userDataManager; + private readonly Func<IMediaSourceManager> _mediaSourceManager; + private readonly IJsonSerializer _json; private ISyncProvider[] _providers = { }; @@ -57,7 +60,7 @@ namespace MediaBrowser.Server.Implementations.Sync public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemUpdated; public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemCreated; - public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager) + public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func<IMediaSourceManager> mediaSourceManager, IJsonSerializer json) { _libraryManager = libraryManager; _repo = repo; @@ -72,6 +75,8 @@ namespace MediaBrowser.Server.Implementations.Sync _subtitleEncoder = subtitleEncoder; _config = config; _userDataManager = userDataManager; + _mediaSourceManager = mediaSourceManager; + _json = json; } public void AddParts(IEnumerable<ISyncProvider> providers) @@ -79,6 +84,19 @@ namespace MediaBrowser.Server.Implementations.Sync _providers = providers.ToArray(); } + public IEnumerable<IServerSyncProvider> ServerSyncProviders + { + get { return _providers.OfType<IServerSyncProvider>(); } + } + + private readonly ConcurrentDictionary<string, ISyncDataProvider> _dataProviders = + new ConcurrentDictionary<string, ISyncDataProvider>(StringComparer.OrdinalIgnoreCase); + + public ISyncDataProvider GetDataProvider(IServerSyncProvider provider, SyncTarget target) + { + return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost.SystemId, _logger, _json, _fileSystem, _config.CommonApplicationPaths)); + } + public async Task<SyncJobCreationResult> CreateJob(SyncJobRequest request) { var processor = GetSyncJobProcessor(); @@ -117,6 +135,14 @@ namespace MediaBrowser.Server.Implementations.Sync 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, @@ -130,9 +156,11 @@ namespace MediaBrowser.Server.Implementations.Sync DateLastModified = DateTime.UtcNow, SyncNewContent = request.SyncNewContent, ItemCount = items.Count, - Quality = request.Quality, Category = request.Category, - ParentId = request.ParentId + ParentId = request.ParentId, + Quality = request.Quality, + Profile = request.Profile, + Bitrate = request.Bitrate }; if (!request.Category.HasValue && request.ItemIds != null) @@ -155,7 +183,7 @@ namespace MediaBrowser.Server.Implementations.Sync // If it already has a converting status then is must have been aborted during conversion var jobItemsResult = _repo.GetJobItems(new SyncJobItemQuery { - Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, JobId = jobId }); @@ -164,7 +192,7 @@ namespace MediaBrowser.Server.Implementations.Sync jobItemsResult = _repo.GetJobItems(new SyncJobItemQuery { - Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Queued, SyncJobItemStatus.Converting }, JobId = jobId }); @@ -193,6 +221,7 @@ namespace MediaBrowser.Server.Implementations.Sync instance.Name = job.Name; instance.Quality = job.Quality; + instance.Profile = job.Profile; instance.UnwatchedOnly = job.UnwatchedOnly; instance.SyncNewContent = job.SyncNewContent; instance.ItemLimit = job.ItemLimit; @@ -407,6 +436,15 @@ namespace MediaBrowser.Server.Implementations.Sync .OrderBy(i => i.Name); } + private IEnumerable<SyncTarget> GetSyncTargets(ISyncProvider provider) + { + return provider.GetAllSyncTargets().Select(i => new SyncTarget + { + Name = i.Name, + Id = GetSyncTargetId(provider, i) + }); + } + private IEnumerable<SyncTarget> GetSyncTargets(ISyncProvider provider, string userId) { return provider.GetSyncTargets(userId).Select(i => new SyncTarget @@ -425,15 +463,9 @@ namespace MediaBrowser.Server.Implementations.Sync return target.Id; } - var providerId = GetSyncProviderId(provider); - return (providerId + "-" + target.Id).GetMD5().ToString("N"); - } - - private ISyncProvider GetSyncProvider(SyncTarget target) - { - var providerId = target.Id.Split(new[] { '-' }, 2).First(); - - return _providers.First(i => string.Equals(providerId, GetSyncProviderId(i))); + return target.Id; + //var providerId = GetSyncProviderId(provider); + //return (providerId + "-" + target.Id).GetMD5().ToString("N"); } private string GetSyncProviderId(ISyncProvider provider) @@ -539,22 +571,6 @@ namespace MediaBrowser.Server.Implementations.Sync return item.Name; } - public DeviceProfile GetDeviceProfile(string targetId) - { - foreach (var provider in _providers) - { - foreach (var target in GetSyncTargets(provider, null)) - { - if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) - { - return provider.GetDeviceProfile(target); - } - } - } - - return null; - } - public async Task ReportSyncJobItemTransferred(string id) { var jobItem = _repo.GetJobItem(id); @@ -586,7 +602,7 @@ namespace MediaBrowser.Server.Implementations.Sync private SyncJobProcessor GetSyncJobProcessor() { - return new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, _mediaEncoder(), _subtitleEncoder(), _config, _fileSystem); + return new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, _mediaEncoder(), _subtitleEncoder(), _config, _fileSystem, _mediaSourceManager()); } public SyncJobItem GetJobItem(string id) @@ -654,8 +670,7 @@ namespace MediaBrowser.Server.Implementations.Sync syncedItem.Item = _dtoService().GetBaseItemDto(libraryItem, dtoOptions); - var mediaSource = syncedItem.Item.MediaSources - .FirstOrDefault(i => string.Equals(i.Id, jobItem.MediaSourceId)); + var mediaSource = jobItem.MediaSource; syncedItem.Item.MediaSources = new List<MediaSourceInfo>(); @@ -704,7 +719,7 @@ namespace MediaBrowser.Server.Implementations.Sync var jobItemResult = GetJobItems(new SyncJobItemQuery { TargetId = targetId, - Statuses = new List<SyncJobItemStatus> + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.ReadyToTransfer } @@ -721,7 +736,7 @@ namespace MediaBrowser.Server.Implementations.Sync var jobItemResult = GetJobItems(new SyncJobItemQuery { TargetId = request.TargetId, - Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Synced } + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Synced } }); var response = new SyncDataResponse(); @@ -967,38 +982,178 @@ namespace MediaBrowser.Server.Implementations.Sync return _repo.GetLibraryItemIds(query); } - public AudioOptions GetAudioOptions(SyncJobItem jobItem) + public SyncJobOptions GetAudioOptions(SyncJobItem jobItem, SyncJob job) { - var profile = GetDeviceProfile(jobItem.TargetId); + var options = GetSyncJobOptions(jobItem.TargetId, null, null); - return new AudioOptions + if (job.Bitrate.HasValue) { - Profile = profile - }; + options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; + } + + return options; } - public VideoOptions GetVideoOptions(SyncJobItem jobItem, SyncJob job) + public SyncJobOptions GetVideoOptions(SyncJobItem jobItem, SyncJob job) { - var profile = GetDeviceProfile(jobItem.TargetId); - var maxBitrate = profile.MaxStaticBitrate; + var options = GetSyncJobOptions(jobItem.TargetId, job.Profile, job.Quality); + + if (job.Bitrate.HasValue) + { + options.DeviceProfile.MaxStaticBitrate = job.Bitrate.Value; + } + + return options; + } - if (maxBitrate.HasValue) + private SyncJobOptions GetSyncJobOptions(string targetId, string profile, string quality) + { + foreach (var provider in _providers) { - if (job.Quality == SyncQuality.Medium) + foreach (var target in GetSyncTargets(provider)) { - maxBitrate = Convert.ToInt32(maxBitrate.Value * .75); + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetSyncJobOptions(provider, target, profile, quality); + } } - else if (job.Quality == SyncQuality.Low) + } + + 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<SyncQualityOption> GetQualityOptions(string targetId) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) { - maxBitrate = Convert.ToInt32(maxBitrate.Value * .5); + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetQualityOptions(provider, target); + } } } - return new VideoOptions + return new List<SyncQualityOption>(); + } + + private IEnumerable<SyncQualityOption> GetQualityOptions(ISyncProvider provider, SyncTarget target) + { + var hasQuality = provider as IHasSyncQuality; + if (hasQuality != null) + { + return hasQuality.GetQualityOptions(target); + } + + // Default options for providers that don't override + return new List<SyncQualityOption> { - Profile = profile, - MaxBitrate = maxBitrate + 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<SyncProfileOption> GetProfileOptions(string targetId) + { + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider)) + { + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return GetProfileOptions(provider, target); + } + } + } + + return new List<SyncProfileOption>(); + } + + private IEnumerable<SyncProfileOption> GetProfileOptions(ISyncProvider provider, SyncTarget target) + { + var hasQuality = provider as IHasSyncQuality; + if (hasQuality != null) + { + return hasQuality.GetProfileOptions(target); + } + + var list = new List<SyncProfileOption>(); + + list.Add(new SyncProfileOption + { + Name = "Original", + Id = "Original", + Description = "Syncs original files as-is.", + EnableQualityOptions = false + }); + + 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; + } } } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs index 05d804cbb..5ad351af5 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Server.Implementations.Sync string[] queries = { - "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Quality TEXT NOT NULL, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", + "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", "create index if not exists idx_SyncJobs on SyncJobs(Id)", "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT)", @@ -64,6 +64,9 @@ namespace MediaBrowser.Server.Implementations.Sync _connection.RunQueries(queries, _logger); + _connection.AddColumn(_logger, "SyncJobs", "Profile", "TEXT"); + _connection.AddColumn(_logger, "SyncJobs", "Bitrate", "INT"); + PrepareStatements(); } @@ -81,12 +84,14 @@ namespace MediaBrowser.Server.Implementations.Sync // _insertJobCommand _insertJobCommand = _connection.CreateCommand(); - _insertJobCommand.CommandText = "insert into SyncJobs (Id, TargetId, Name, Quality, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Quality, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; + _insertJobCommand.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; _insertJobCommand.Parameters.Add(_insertJobCommand, "@Id"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@TargetId"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@Name"); + _insertJobCommand.Parameters.Add(_insertJobCommand, "@Profile"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@Quality"); + _insertJobCommand.Parameters.Add(_insertJobCommand, "@Bitrate"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@Status"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@Progress"); _insertJobCommand.Parameters.Add(_insertJobCommand, "@UserId"); @@ -102,12 +107,14 @@ namespace MediaBrowser.Server.Implementations.Sync // _updateJobCommand _updateJobCommand = _connection.CreateCommand(); - _updateJobCommand.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Quality=@Quality,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@ID"; + _updateJobCommand.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@ID"; _updateJobCommand.Parameters.Add(_updateJobCommand, "@Id"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@TargetId"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@Name"); + _updateJobCommand.Parameters.Add(_updateJobCommand, "@Profile"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@Quality"); + _updateJobCommand.Parameters.Add(_updateJobCommand, "@Bitrate"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@Status"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@Progress"); _updateJobCommand.Parameters.Add(_updateJobCommand, "@UserId"); @@ -162,7 +169,7 @@ namespace MediaBrowser.Server.Implementations.Sync _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@JobItemIndex"); } - private const string BaseJobSelectText = "select Id, TargetId, Name, Quality, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; + private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex from SyncJobItems"; public SyncJob GetJob(string id) @@ -210,54 +217,64 @@ namespace MediaBrowser.Server.Implementations.Sync if (!reader.IsDBNull(3)) { - info.Quality = (SyncQuality)Enum.Parse(typeof(SyncQuality), reader.GetString(3), true); + info.Profile = reader.GetString(3); } if (!reader.IsDBNull(4)) { - info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(4), true); + info.Quality = reader.GetString(4); } if (!reader.IsDBNull(5)) { - info.Progress = reader.GetDouble(5); + info.Bitrate = reader.GetInt32(5); } if (!reader.IsDBNull(6)) { - info.UserId = reader.GetString(6); + info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(6), true); } if (!reader.IsDBNull(7)) { - info.RequestedItemIds = reader.GetString(7).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + info.Progress = reader.GetDouble(7); } if (!reader.IsDBNull(8)) { - info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(8), true); + info.UserId = reader.GetString(8); } if (!reader.IsDBNull(9)) { - info.ParentId = reader.GetString(9); + info.RequestedItemIds = reader.GetString(9).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } if (!reader.IsDBNull(10)) { - info.UnwatchedOnly = reader.GetBoolean(10); + info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(10), true); } if (!reader.IsDBNull(11)) { - info.ItemLimit = reader.GetInt32(11); + info.ParentId = reader.GetString(11); } - info.SyncNewContent = reader.GetBoolean(12); + if (!reader.IsDBNull(12)) + { + info.UnwatchedOnly = reader.GetBoolean(12); + } + + if (!reader.IsDBNull(13)) + { + info.ItemLimit = reader.GetInt32(13); + } - info.DateCreated = reader.GetDateTime(13).ToUniversalTime(); - info.DateLastModified = reader.GetDateTime(14).ToUniversalTime(); - info.ItemCount = reader.GetInt32(15); + info.SyncNewContent = reader.GetBoolean(14); + + info.DateCreated = reader.GetDateTime(15).ToUniversalTime(); + info.DateLastModified = reader.GetDateTime(16).ToUniversalTime(); + info.ItemCount = reader.GetInt32(17); return info; } @@ -294,7 +311,9 @@ namespace MediaBrowser.Server.Implementations.Sync cmd.GetParameter(index++).Value = new Guid(job.Id); cmd.GetParameter(index++).Value = job.TargetId; cmd.GetParameter(index++).Value = job.Name; + cmd.GetParameter(index++).Value = job.Profile; cmd.GetParameter(index++).Value = job.Quality; + cmd.GetParameter(index++).Value = job.Bitrate; cmd.GetParameter(index++).Value = job.Status.ToString(); cmd.GetParameter(index++).Value = job.Progress; cmd.GetParameter(index++).Value = job.UserId; @@ -421,7 +440,7 @@ namespace MediaBrowser.Server.Implementations.Sync var whereClauses = new List<string>(); - if (query.Statuses.Count > 0) + if (query.Statuses.Length > 0) { var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); @@ -430,6 +449,7 @@ namespace MediaBrowser.Server.Implementations.Sync if (!string.IsNullOrWhiteSpace(query.TargetId)) { whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; } if (!string.IsNullOrWhiteSpace(query.UserId)) { @@ -442,6 +462,8 @@ namespace MediaBrowser.Server.Implementations.Sync cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; } + cmd.CommandText += " mainTable"; + var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); @@ -449,7 +471,7 @@ namespace MediaBrowser.Server.Implementations.Sync var startIndex = query.StartIndex ?? 0; if (startIndex > 0) { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=@TargetId) DESC, DateLastModified DESC LIMIT {0})", + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", startIndex.ToString(_usCulture))); } @@ -458,8 +480,7 @@ namespace MediaBrowser.Server.Implementations.Sync cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); } - cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=@TargetId) DESC, DateLastModified DESC"; - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; if (query.Limit.HasValue) { @@ -539,13 +560,18 @@ namespace MediaBrowser.Server.Implementations.Sync whereClauses.Add("JobId=@JobId"); cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; } + if (!string.IsNullOrWhiteSpace(query.ItemId)) + { + whereClauses.Add("ItemId=@ItemId"); + cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; + } if (!string.IsNullOrWhiteSpace(query.TargetId)) { whereClauses.Add("TargetId=@TargetId"); cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; } - if (query.Statuses.Count > 0) + if (query.Statuses.Length > 0) { var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs new file mode 100644 index 000000000..893b16b14 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs @@ -0,0 +1,68 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +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; + + public SyncedMediaSourceProvider(ISyncManager syncManager, IServerApplicationHost appHost) + { + _appHost = appHost; + _syncManager = (SyncManager)syncManager; + } + + public async Task<IEnumerable<MediaSourceInfo>> GetMediaSources(IHasMediaSources item, CancellationToken cancellationToken) + { + var jobItemResult = _syncManager.GetJobItems(new SyncJobItemQuery + { + AddMetadata = false, + Statuses = new SyncJobItemStatus[] { SyncJobItemStatus.Synced }, + ItemId = item.Id.ToString("N") + }); + + var list = new List<MediaSourceInfo>(); + + if (jobItemResult.Items.Length > 0) + { + var targets = _syncManager.ServerSyncProviders + .SelectMany(i => i.GetAllSyncTargets().Select(t => new Tuple<IServerSyncProvider, SyncTarget>(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 dataProvider = _syncManager.GetDataProvider(targetTuple.Item1, syncTarget); + + var localItems = await dataProvider.GetCachedItems(syncTarget, serverId, item.Id.ToString("N")).ConfigureAwait(false); + + foreach (var localItem in localItems) + { + list.AddRange(localItem.Item.MediaSources); + } + } + } + } + + return list; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs new file mode 100644 index 000000000..ca9d96c12 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs @@ -0,0 +1,243 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +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<LocalItem> _items; + + private readonly ILogger _logger; + private readonly IJsonSerializer _json; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly string _serverId; + + private readonly SemaphoreSlim _cacheFileLock = new SemaphoreSlim(1, 1); + + public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, string serverId, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths) + { + _logger = logger; + _json = json; + _provider = provider; + _target = target; + _fileSystem = fileSystem; + _appPaths = appPaths; + _serverId = serverId; + } + + private string GetCachePath() + { + return Path.Combine(_appPaths.DataPath, "sync", _target.Id.GetMD5().ToString("N") + ".json"); + } + + private string GetRemotePath() + { + var parts = new List<string> + { + _serverId, + "data.json" + }; + + return _provider.GetFullPath(parts, _target); + } + + private async Task CacheData(Stream stream) + { + var cachePath = GetCachePath(); + + await _cacheFileLock.WaitAsync().ConfigureAwait(false); + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(cachePath)); + using (var fileStream = _fileSystem.GetFileStream(cachePath, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + { + await stream.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error saving sync data to {0}", ex, cachePath); + } + finally + { + _cacheFileLock.Release(); + } + } + + private async Task EnsureData(CancellationToken cancellationToken) + { + if (_items == null) + { + try + { + using (var stream = await _provider.GetFile(GetRemotePath(), _target, new Progress<double>(), cancellationToken)) + { + _items = _json.DeserializeFromStream<List<LocalItem>>(stream); + } + } + catch (FileNotFoundException) + { + _items = new List<LocalItem>(); + } + catch (DirectoryNotFoundException) + { + _items = new List<LocalItem>(); + } + + using (var memoryStream = new MemoryStream()) + { + _json.SerializeToStream(_items, memoryStream); + + // Now cache it + memoryStream.Position = 0; + await CacheData(memoryStream).ConfigureAwait(false); + } + } + } + + private async Task SaveData(CancellationToken cancellationToken) + { + using (var stream = new MemoryStream()) + { + _json.SerializeToStream(_items, stream); + + // Save to sync provider + stream.Position = 0; + await _provider.SendFile(stream, GetRemotePath(), _target, new Progress<double>(), cancellationToken).ConfigureAwait(false); + + // Now cache it + stream.Position = 0; + await CacheData(stream).ConfigureAwait(false); + } + } + + private async Task<T> GetData<T>(Func<List<LocalItem>, T> dataFactory) + { + await _dataLock.WaitAsync().ConfigureAwait(false); + + try + { + await EnsureData(CancellationToken.None).ConfigureAwait(false); + + return dataFactory(_items); + } + finally + { + _dataLock.Release(); + } + } + + private async Task UpdateData(Func<List<LocalItem>, List<LocalItem>> action) + { + await _dataLock.WaitAsync().ConfigureAwait(false); + + try + { + await EnsureData(CancellationToken.None).ConfigureAwait(false); + + _items = action(_items); + + await SaveData(CancellationToken.None).ConfigureAwait(false); + } + finally + { + _dataLock.Release(); + } + } + + public Task<List<string>> GetServerItemIds(SyncTarget target, string serverId) + { + return GetData(items => items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase)).Select(i => i.ItemId).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<LocalItem> Get(SyncTarget target, string id) + { + return GetData(items => items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase))); + } + + private async Task<List<LocalItem>> GetCachedData() + { + if (_items == null) + { + await _cacheFileLock.WaitAsync().ConfigureAwait(false); + + try + { + if (_items == null) + { + try + { + _items = _json.DeserializeFromFile<List<LocalItem>>(GetCachePath()); + } + catch (FileNotFoundException) + { + _items = new List<LocalItem>(); + } + catch (DirectoryNotFoundException) + { + _items = new List<LocalItem>(); + } + } + } + finally + { + _cacheFileLock.Release(); + } + } + + return _items.ToList(); + } + + public async Task<List<string>> GetCachedServerItemIds(SyncTarget target, string serverId) + { + var items = await GetCachedData().ConfigureAwait(false); + + return items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase)) + .Select(i => i.ItemId) + .ToList(); + } + + public async Task<List<LocalItem>> GetCachedItems(SyncTarget target, string serverId, string itemId) + { + var items = await GetCachedData().ConfigureAwait(false); + + return items.Where(i => string.Equals(i.ServerId, serverId, StringComparison.OrdinalIgnoreCase) && string.Equals(i.ItemId, itemId, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + } +} |
