diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations/Sync')
7 files changed, 1253 insertions, 135 deletions
diff --git a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs index c3cd047b6b..6cc5be9555 100644 --- a/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/AppSyncProvider.cs @@ -1,21 +1,54 @@ -using MediaBrowser.Controller.Sync; +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 + public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds { + private readonly IDeviceManager _deviceManager; + + public AppSyncProvider(IDeviceManager deviceManager) + { + _deviceManager = deviceManager; + } + public IEnumerable<SyncTarget> GetSyncTargets() { - return new List<SyncTarget>(); + return _deviceManager.GetDevices(new DeviceQuery + { + SupportsSync = true + + }).Items.Select(i => new SyncTarget + { + Id = i.Id, + Name = i.Name + }); + } + + public IEnumerable<SyncTarget> 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) { - return new DeviceProfile(); + var caps = _deviceManager.GetCapabilities(target.Id); + + return caps == null || caps.DeviceProfile == null ? new DeviceProfile() : caps.DeviceProfile; } public string Name diff --git a/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs index fd12b1f8aa..da3ecdfa66 100644 --- a/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/CloudSyncProvider.cs @@ -2,7 +2,6 @@ using MediaBrowser.Controller.Sync; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Sync; -using System; using System.Collections.Generic; using System.Linq; @@ -10,7 +9,7 @@ namespace MediaBrowser.Server.Implementations.Sync { public class CloudSyncProvider : ISyncProvider { - private ICloudSyncProvider[] _providers = new ICloudSyncProvider[] {}; + private ICloudSyncProvider[] _providers = {}; public CloudSyncProvider(IApplicationHost appHost) { @@ -22,6 +21,11 @@ namespace MediaBrowser.Server.Implementations.Sync return new List<SyncTarget>(); } + public IEnumerable<SyncTarget> GetSyncTargets(string userId) + { + return new List<SyncTarget>(); + } + public DeviceProfile GetDeviceProfile(SyncTarget target) { return new DeviceProfile(); diff --git a/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs b/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs deleted file mode 100644 index bc079ad809..0000000000 --- a/MediaBrowser.Server.Implementations/Sync/MockSyncProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Sync; -using System.Collections.Generic; - -namespace MediaBrowser.Server.Implementations.Sync -{ - public class MockSyncProvider : ISyncProvider - { - public string Name - { - get { return "Dummy Sync"; } - } - - public IEnumerable<SyncTarget> GetSyncTargets() - { - return new List<SyncTarget> - { - new SyncTarget - { - Id = "mock".GetMD5().ToString("N"), - Name = "Mock Sync" - } - }; - } - - public DeviceProfile GetDeviceProfile(SyncTarget target) - { - return new DeviceProfile(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs new file mode 100644 index 0000000000..896e49cb21 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs @@ -0,0 +1,506 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +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 MoreLinq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public class SyncJobProcessor + { + 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; + + public SyncJobProcessor(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder) + { + _libraryManager = libraryManager; + _syncRepo = syncRepo; + _syncManager = syncManager; + _logger = logger; + _userManager = userManager; + _tvSeriesManager = tvSeriesManager; + MediaEncoder = mediaEncoder; + } + + 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 = _syncRepo.GetJobItems(new SyncJobItemQuery + { + JobId = job.Id + + }).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; + } + + jobItem = new SyncJobItem + { + Id = Guid.NewGuid().ToString("N"), + ItemId = itemId, + ItemName = GetSyncJobItemName(item), + JobId = job.Id, + TargetId = job.TargetId, + DateCreated = DateTime.UtcNow + }; + + await _syncRepo.Create(jobItem).ConfigureAwait(false); + + jobItems.Add(jobItem); + } + + jobItems = jobItems + .OrderBy(i => i.DateCreated) + .ToList(); + + await UpdateJobStatus(job, jobItems).ConfigureAwait(false); + } + + private string GetSyncJobItemName(BaseItem item) + { + return item.Name; + } + + public Task UpdateJobStatus(string id) + { + var job = _syncRepo.GetJob(id); + + return UpdateJobStatus(job); + } + + private Task UpdateJobStatus(SyncJob job) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + var result = _syncRepo.GetJobItems(new SyncJobItemQuery + { + JobId = job.Id + }); + + return UpdateJobStatus(job, result.Items.ToList()); + } + + private Task UpdateJobStatus(SyncJob job, List<SyncJobItem> 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) + { + pct += 100; + } + else + { + pct += item.Progress ?? 0; + } + } + + if (job.ItemCount > 0) + { + pct /= job.ItemCount; + job.Progress = pct; + } + else + { + job.Progress = null; + } + + if (pct >= 100) + { + if (jobItems.Any(i => i.Status == SyncJobItemStatus.Failed)) + { + job.Status = SyncJobStatus.CompletedWithError; + } + else + { + job.Status = SyncJobStatus.Completed; + } + } + else if (pct.Equals(0)) + { + job.Status = SyncJobStatus.Queued; + } + else + { + job.Status = SyncJobStatus.InProgress; + } + + return _syncRepo.Update(job); + } + + public async Task<IEnumerable<BaseItem>> GetItemsForSync(SyncCategory? category, string parentId, IEnumerable<string> itemIds, User user, bool unwatchedOnly) + { + var items = category.HasValue ? + await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false) : + itemIds.SelectMany(i => GetItemsForSync(i, user)) + .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<IEnumerable<BaseItem>> 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); + } + + query.User = user; + + var result = await parent.GetItems(query).ConfigureAwait(false); + return result.Items; + } + + private IEnumerable<BaseItem> GetItemsForSync(string id, User user) + { + var item = _libraryManager.GetItemById(id); + + if (item == null) + { + return new List<BaseItem>(); + } + + return GetItemsForSync(item, user); + } + + private IEnumerable<BaseItem> GetItemsForSync(BaseItem item, User user) + { + var itemByName = item as IItemByName; + if (itemByName != null) + { + var items = user.RootFolder + .GetRecursiveChildren(user); + + return itemByName.GetTaggedItems(items); + } + + if (item.IsFolder) + { + var folder = (Folder)item; + var items = folder.GetRecursiveChildren(user); + + items = items.Where(i => !i.IsFolder); + + if (!folder.IsPreSorted) + { + items = items.OrderBy(i => i.SortName); + } + + return items; + } + + return new[] { item }; + } + + public async Task EnsureSyncJobs(CancellationToken cancellationToken) + { + var jobResult = _syncRepo.GetJobs(new SyncJobQuery + { + IsCompleted = false + }); + + foreach (var job in jobResult.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (job.SyncNewContent) + { + await EnsureJobItems(job).ConfigureAwait(false); + } + } + } + + public async Task Sync(IProgress<double> progress, CancellationToken cancellationToken) + { + await EnsureSyncJobs(cancellationToken).ConfigureAwait(false); + + // 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 } + }); + + var jobItems = result.Items; + var index = 0; + + foreach (var item in jobItems) + { + double percent = index; + percent /= result.TotalRecordCount; + + progress.Report(100 * percent); + + cancellationToken.ThrowIfCancellationRequested(); + + await ProcessJobItem(item, cancellationToken).ConfigureAwait(false); + + var job = _syncRepo.GetJob(item.JobId); + await UpdateJobStatus(job).ConfigureAwait(false); + + index++; + } + } + + private async Task ProcessJobItem(SyncJobItem jobItem, CancellationToken cancellationToken) + { + 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 _syncRepo.Update(jobItem).ConfigureAwait(false); + 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 _syncRepo.Update(jobItem).ConfigureAwait(false); + return; + } + + jobItem.Progress = 0; + jobItem.Status = SyncJobItemStatus.Converting; + + var video = item as Video; + if (video != null) + { + await Sync(jobItem, video, deviceProfile, cancellationToken).ConfigureAwait(false); + } + + else if (item is Audio) + { + await Sync(jobItem, (Audio)item, deviceProfile, cancellationToken).ConfigureAwait(false); + } + + else if (item is Photo) + { + await Sync(jobItem, (Photo)item, deviceProfile, cancellationToken).ConfigureAwait(false); + } + + else + { + await SyncGeneric(jobItem, item, deviceProfile, cancellationToken).ConfigureAwait(false); + } + } + + private async Task Sync(SyncJobItem jobItem, Video item, DeviceProfile profile, CancellationToken cancellationToken) + { + var options = new VideoOptions + { + Context = EncodingContext.Static, + ItemId = item.Id.ToString("N"), + DeviceId = jobItem.TargetId, + Profile = profile, + MediaSources = item.GetMediaSources(false).ToList() + }; + + var streamInfo = new StreamBuilder().BuildVideoItem(options); + var mediaSource = streamInfo.MediaSource; + + jobItem.MediaSourceId = streamInfo.MediaSourceId; + + if (streamInfo.PlayMethod == PlayMethod.Transcode) + { + jobItem.Status = SyncJobItemStatus.Converting; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + + jobItem.OutputPath = await MediaEncoder.EncodeVideo(new EncodingJobOptions(streamInfo, profile), new Progress<double>(), cancellationToken); + } + 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.Progress = 50; + jobItem.Status = SyncJobItemStatus.Transferring; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + } + + private async Task Sync(SyncJobItem jobItem, Audio item, DeviceProfile profile, CancellationToken cancellationToken) + { + var options = new AudioOptions + { + Context = EncodingContext.Static, + ItemId = item.Id.ToString("N"), + DeviceId = jobItem.TargetId, + Profile = profile, + MediaSources = item.GetMediaSources(false).ToList() + }; + + var streamInfo = new StreamBuilder().BuildAudioItem(options); + var mediaSource = streamInfo.MediaSource; + + jobItem.MediaSourceId = streamInfo.MediaSourceId; + + if (streamInfo.PlayMethod == PlayMethod.Transcode) + { + jobItem.Status = SyncJobItemStatus.Converting; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + + jobItem.OutputPath = await MediaEncoder.EncodeAudio(new EncodingJobOptions(streamInfo, profile), new Progress<double>(), cancellationToken); + } + 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.Progress = 50; + jobItem.Status = SyncJobItemStatus.Transferring; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + } + + private async Task Sync(SyncJobItem jobItem, Photo item, DeviceProfile profile, CancellationToken cancellationToken) + { + jobItem.OutputPath = item.Path; + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.Transferring; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + } + + private async Task SyncGeneric(SyncJobItem jobItem, BaseItem item, DeviceProfile profile, CancellationToken cancellationToken) + { + jobItem.OutputPath = item.Path; + + jobItem.Progress = 50; + jobItem.Status = SyncJobItemStatus.Transferring; + await _syncRepo.Update(jobItem).ConfigureAwait(false); + } + + private async Task<string> DownloadFile(SyncJobItem jobItem, MediaSourceInfo mediaSource, CancellationToken cancellationToken) + { + // TODO: Download + return mediaSource.Path; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index b5e13e306b..68eaa38d35 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -1,16 +1,27 @@ -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Channels; 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.LiveTv; +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.Querying; using MediaBrowser.Model.Sync; +using MediaBrowser.Model.Users; using MoreLinq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; @@ -22,15 +33,25 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly ISyncRepository _repo; private readonly IImageProcessor _imageProcessor; private readonly ILogger _logger; + private readonly IUserManager _userManager; + private readonly Func<IDtoService> _dtoService; + private readonly IApplicationHost _appHost; + private readonly ITVSeriesManager _tvSeriesManager; + private readonly Func<IMediaEncoder> MediaEncoder; private ISyncProvider[] _providers = { }; - public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger) + public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder) { _libraryManager = libraryManager; _repo = repo; _imageProcessor = imageProcessor; _logger = logger; + _userManager = userManager; + _dtoService = dtoService; + _appHost = appHost; + _tvSeriesManager = tvSeriesManager; + MediaEncoder = mediaEncoder; } public void AddParts(IEnumerable<ISyncProvider> providers) @@ -40,11 +61,25 @@ namespace MediaBrowser.Server.Implementations.Sync public async Task<SyncJobCreationResult> CreateJob(SyncJobRequest request) { - var items = GetItemsForSync(request.ItemIds).ToList(); + var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, MediaEncoder()); - if (items.Count == 1) + 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))) { - request.Name = GetDefaultName(items[0]); + 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)) @@ -53,7 +88,12 @@ namespace MediaBrowser.Server.Implementations.Sync } var target = GetSyncTargets(request.UserId) - .First(i => string.Equals(request.TargetId, i.Id)); + .FirstOrDefault(i => string.Equals(request.TargetId, i.Id)); + + if (target == null) + { + throw new ArgumentException("Sync target not found."); + } var jobId = Guid.NewGuid().ToString("N"); @@ -64,35 +104,75 @@ namespace MediaBrowser.Server.Implementations.Sync TargetId = target.Id, UserId = request.UserId, UnwatchedOnly = request.UnwatchedOnly, - Limit = request.Limit, - LimitType = request.LimitType, - RequestedItemIds = request.ItemIds, + ItemLimit = request.ItemLimit, + RequestedItemIds = request.ItemIds ?? new List<string> { }, DateCreated = DateTime.UtcNow, DateLastModified = DateTime.UtcNow, - ItemCount = 1 + SyncNewContent = request.SyncNewContent, + ItemCount = items.Count, + Quality = request.Quality, + Category = request.Category, + ParentId = request.ParentId }; + // It's just a static list + if (!items.Any(i => i.IsFolder || i is IItemByName)) + { + job.SyncNewContent = false; + } + await _repo.Create(job).ConfigureAwait(false); + await processor.EnsureJobItems(job).ConfigureAwait(false); + return new SyncJobCreationResult { Job = GetJob(jobId) }; } - public QueryResult<SyncJob> GetJobs(SyncJobQuery query) + public 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.UnwatchedOnly = job.UnwatchedOnly; + instance.SyncNewContent = job.SyncNewContent; + instance.ItemLimit = job.ItemLimit; + + return _repo.Update(instance); + } + + public async Task<QueryResult<SyncJob>> GetJobs(SyncJobQuery query) { var result = _repo.GetJobs(query); - result.Items.ForEach(FillMetadata); + foreach (var item in result.Items) + { + await FillMetadata(item).ConfigureAwait(false); + } return result; } - private void FillMetadata(SyncJob job) + private async Task FillMetadata(SyncJob job) { - var item = GetItemsForSync(job.RequestedItemIds) - .FirstOrDefault(); + var item = job.RequestedItemIds + .Select(_libraryManager.GetItemById) + .FirstOrDefault(i => i != null); + + if (item == null) + { + var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, MediaEncoder()); + + var user = _userManager.GetUserById(job.UserId); + + item = (await processor + .GetItemsForSync(job.Category, job.ParentId, job.RequestedItemIds, user, job.UnwatchedOnly).ConfigureAwait(false)) + .FirstOrDefault(); + } if (item != null) { @@ -109,13 +189,25 @@ namespace MediaBrowser.Server.Implementations.Sync } var primaryImage = item.GetImageInfo(ImageType.Primary, 0); + var itemWithImage = item; + + if (primaryImage == null) + { + var parentWithImage = item.Parents.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(item, ImageType.Primary); - job.PrimaryImageItemId = item.Id.ToString("N"); + job.PrimaryImageTag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Primary); + job.PrimaryImageItemId = itemWithImage.Id.ToString("N"); } catch (Exception ex) @@ -126,9 +218,47 @@ namespace MediaBrowser.Server.Implementations.Sync } } + 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.Parents.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 Task CancelJob(string id) { - throw new NotImplementedException(); + return _repo.DeleteJob(id); } public SyncJob GetJob(string id) @@ -145,15 +275,26 @@ namespace MediaBrowser.Server.Implementations.Sync private IEnumerable<SyncTarget> GetSyncTargets(ISyncProvider provider, string userId) { - var providerId = GetSyncProviderId(provider); - - return provider.GetSyncTargets().Select(i => new SyncTarget + return provider.GetSyncTargets(userId).Select(i => new SyncTarget { Name = i.Name, - Id = providerId + "-" + i.Id + Id = GetSyncTargetId(provider, i) }); } + private string GetSyncTargetId(ISyncProvider provider, SyncTarget target) + { + var hasUniqueId = provider as IHasUniqueTargetIds; + + if (hasUniqueId != null) + { + 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(); @@ -163,75 +304,249 @@ namespace MediaBrowser.Server.Implementations.Sync private string GetSyncProviderId(ISyncProvider provider) { - return (provider.GetType().Name + provider.Name).GetMD5().ToString("N"); + return (provider.GetType().Name).GetMD5().ToString("N"); } public bool SupportsSync(BaseItem item) { - if (item.LocationType == LocationType.Virtual) + 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)) { - return false; - } + if (item.LocationType == LocationType.Virtual) + { + return false; + } - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) || - string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - if (item.RunTimeTicks.HasValue) + if (!item.RunTimeTicks.HasValue) { - var video = item as Video; + return false; + } - if (video != null) + var video = item as Video; + if (video != null) + { + if (video.VideoType == VideoType.Iso) { - if (video.VideoType != VideoType.VideoFile) - { - return false; - } + return false; + } - if (video.IsMultiPart) - { - return false; - } + if (video.IsStacked) + { + return false; } + } - return true; + var game = item as Game; + if (game != null) + { + if (game.IsMultiPart) + { + return false; + } } - return false; + if (item is LiveTvChannel || item is IChannelItem || item is ILiveTvRecording) + { + return false; + } + + // It would be nice to support these later + if (item is Game || item is Book) + { + return false; + } + + return true; } - return false; + return item.LocationType == LocationType.FileSystem || item is Season || item is ILiveTvRecording; } - private IEnumerable<BaseItem> GetItemsForSync(IEnumerable<string> itemIds) + private string GetDefaultName(BaseItem item) { - return itemIds.SelectMany(GetItemsForSync).DistinctBy(i => i.Id); + return item.Name; } - private IEnumerable<BaseItem> GetItemsForSync(string id) + public DeviceProfile GetDeviceProfile(string targetId) { - var item = _libraryManager.GetItemById(id); + foreach (var provider in _providers) + { + foreach (var target in GetSyncTargets(provider, null)) + { + if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase)) + { + return provider.GetDeviceProfile(target); + } + } + } - if (item == null) + return null; + } + + public async Task ReportSyncJobItemTransferred(string id) + { + var jobItem = _repo.GetJobItem(id); + + jobItem.Status = SyncJobItemStatus.Synced; + jobItem.Progress = 100; + + await _repo.Update(jobItem).ConfigureAwait(false); + + var processor = new SyncJobProcessor(_libraryManager, _repo, this, _logger, _userManager, _tvSeriesManager, MediaEncoder()); + + await processor.UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); + } + + public SyncJobItem GetJobItem(string id) + { + return _repo.GetJobItem(id); + } + + public QueryResult<SyncJobItem> GetJobItems(SyncJobItemQuery query) + { + var result = _repo.GetJobItems(query); + + if (query.AddMetadata) { - throw new ArgumentException("Item with Id " + id + " not found."); + result.Items.ForEach(FillMetadata); } - if (!SupportsSync(item)) + return result; + } + + private SyncedItem GetJobItemInfo(SyncJobItem jobItem) + { + var job = _repo.GetJob(jobItem.JobId); + + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + + var syncedItem = new SyncedItem + { + SyncJobId = jobItem.JobId, + SyncJobItemId = jobItem.Id, + ServerId = _appHost.SystemId, + UserId = job.UserId + }; + + 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.OriginalPrimaryImageAspectRatio); + dtoOptions.Fields.Remove(ItemFields.Path); + dtoOptions.Fields.Remove(ItemFields.SeriesGenres); + dtoOptions.Fields.Remove(ItemFields.Settings); + dtoOptions.Fields.Remove(ItemFields.SyncInfo); + + syncedItem.Item = _dtoService().GetBaseItemDto(libraryItem, dtoOptions); + + // TODO: this should be the media source of the transcoded output + syncedItem.Item.MediaSources = syncedItem.Item.MediaSources + .Where(i => string.Equals(i.Id, jobItem.MediaSourceId)) + .ToList(); + + var mediaSource = syncedItem.Item.MediaSources + .FirstOrDefault(i => string.Equals(i.Id, jobItem.MediaSourceId)); + + // This will be null for items that are not audio/video + if (mediaSource == null) + { + syncedItem.OriginalFileName = Path.GetFileName(libraryItem.Path); + } + else { - throw new ArgumentException("Item with Id " + id + " does not support sync."); + syncedItem.OriginalFileName = Path.GetFileName(mediaSource.Path); } - return GetItemsForSync(item); + return syncedItem; } - private IEnumerable<BaseItem> GetItemsForSync(BaseItem item) + public Task ReportOfflineAction(UserAction action) { - return new[] { item }; + return Task.FromResult(true); } - private string GetDefaultName(BaseItem item) + public List<SyncedItem> GetReadySyncItems(string targetId) { - return item.Name; + var jobItemResult = GetJobItems(new SyncJobItemQuery + { + TargetId = targetId, + Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Transferring } + }); + + return jobItemResult.Items.Select(GetJobItemInfo) + .ToList(); + } + + public async Task<SyncDataResponse> SyncData(SyncDataRequest request) + { + var jobItemResult = GetJobItems(new SyncJobItemQuery + { + TargetId = request.TargetId, + Statuses = new List<SyncJobItemStatus> { SyncJobItemStatus.Synced } + }); + + var response = new SyncDataResponse(); + + foreach (var jobItem in jobItemResult.Items) + { + if (request.LocalItemIds.Contains(jobItem.ItemId, StringComparer.OrdinalIgnoreCase)) + { + var job = _repo.GetJob(jobItem.JobId); + var user = _userManager.GetUserById(job.UserId); + + if (user == null) + { + // Tell the device to remove it since the user is gone now + response.ItemIdsToRemove.Add(jobItem.ItemId); + } + else if (job.UnwatchedOnly) + { + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + + if (IsLibraryItemAvailable(libraryItem)) + { + if (libraryItem.IsPlayed(user) && libraryItem is Video) + { + // Tell the device to remove it since it has been played + response.ItemIdsToRemove.Add(jobItem.ItemId); + } + } + else + { + // Tell the device to remove it since it's no longer available + response.ItemIdsToRemove.Add(jobItem.ItemId); + } + } + } + else + { + // Content is no longer on the device + jobItem.Status = SyncJobItemStatus.RemovedFromDevice; + await _repo.Update(jobItem).ConfigureAwait(false); + } + } + + response.ItemIdsToRemove = response.ItemIdsToRemove.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + return response; + } + + private bool IsLibraryItemAvailable(BaseItem item) + { + if (item == null) + { + return false; + } + + // TODO: Make sure it hasn't been deleted + + return true; } } } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs index 65da74f9e5..09c1b316ef 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs @@ -23,6 +23,8 @@ namespace MediaBrowser.Server.Implementations.Sync private readonly IServerApplicationPaths _appPaths; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private IDbCommand _deleteJobCommand; + private IDbCommand _deleteJobItemsCommand; private IDbCommand _saveJobCommand; private IDbCommand _saveJobItemCommand; @@ -34,16 +36,16 @@ namespace MediaBrowser.Server.Implementations.Sync public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "sync.db"); + var dbFile = Path.Combine(_appPaths.DataPath, "sync10.db"); _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); 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, UnwatchedOnly BIT, SyncLimit BigInt, LimitType TEXT, IsDynamic 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, 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 index if not exists idx_SyncJobs on SyncJobs(Id)", - "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, JobId TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT)", + "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT)", "create index if not exists idx_SyncJobItems on SyncJobs(Id)", //pragmas @@ -59,8 +61,16 @@ namespace MediaBrowser.Server.Implementations.Sync private void PrepareStatements() { + _deleteJobCommand = _connection.CreateCommand(); + _deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; + _deleteJobCommand.Parameters.Add(_deleteJobCommand, "@Id"); + + _deleteJobItemsCommand = _connection.CreateCommand(); + _deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; + _deleteJobItemsCommand.Parameters.Add(_deleteJobItemsCommand, "@JobId"); + _saveJobCommand = _connection.CreateCommand(); - _saveJobCommand.CommandText = "replace into SyncJobs (Id, TargetId, Name, Quality, Status, Progress, UserId, ItemIds, UnwatchedOnly, SyncLimit, LimitType, IsDynamic, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Quality, @Status, @Progress, @UserId, @ItemIds, @UnwatchedOnly, @SyncLimit, @LimitType, @IsDynamic, @DateCreated, @DateLastModified, @ItemCount)"; + _saveJobCommand.CommandText = "replace 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)"; _saveJobCommand.Parameters.Add(_saveJobCommand, "@Id"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@TargetId"); @@ -70,26 +80,32 @@ namespace MediaBrowser.Server.Implementations.Sync _saveJobCommand.Parameters.Add(_saveJobCommand, "@Progress"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@UserId"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@ItemIds"); + _saveJobCommand.Parameters.Add(_saveJobCommand, "@Category"); + _saveJobCommand.Parameters.Add(_saveJobCommand, "@ParentId"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@UnwatchedOnly"); - _saveJobCommand.Parameters.Add(_saveJobCommand, "@SyncLimit"); - _saveJobCommand.Parameters.Add(_saveJobCommand, "@LimitType"); - _saveJobCommand.Parameters.Add(_saveJobCommand, "@IsDynamic"); + _saveJobCommand.Parameters.Add(_saveJobCommand, "@ItemLimit"); + _saveJobCommand.Parameters.Add(_saveJobCommand, "@SyncNewContent"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@DateCreated"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@DateLastModified"); _saveJobCommand.Parameters.Add(_saveJobCommand, "@ItemCount"); _saveJobItemCommand = _connection.CreateCommand(); - _saveJobItemCommand.CommandText = "replace into SyncJobItems (Id, ItemId, JobId, OutputPath, Status, TargetId) values (@Id, @ItemId, @JobId, @OutputPath, @Status, @TargetId)"; - - _saveJobItemCommand.Parameters.Add(_saveJobCommand, "@Id"); - _saveJobItemCommand.Parameters.Add(_saveJobCommand, "@ItemId"); - _saveJobItemCommand.Parameters.Add(_saveJobCommand, "@JobId"); - _saveJobItemCommand.Parameters.Add(_saveJobCommand, "@OutputPath"); - _saveJobItemCommand.Parameters.Add(_saveJobCommand, "@Status"); + _saveJobItemCommand.CommandText = "replace into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, OutputPath, Status, TargetId, DateCreated, Progress) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @OutputPath, @Status, @TargetId, @DateCreated, @Progress)"; + + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@Id"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@ItemId"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@ItemName"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@MediaSourceId"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@JobId"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@OutputPath"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@Status"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@TargetId"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@DateCreated"); + _saveJobItemCommand.Parameters.Add(_saveJobItemCommand, "@Progress"); } - private const string BaseJobSelectText = "select Id, TargetId, Name, Quality, Status, Progress, UserId, ItemIds, UnwatchedOnly, SyncLimit, LimitType, IsDynamic, DateCreated, DateLastModified, ItemCount from SyncJobs"; - private const string BaseJobItemSelectText = "select Id, ItemId, JobId, OutputPath, Status, TargetId from SyncJobItems"; + 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 BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, OutputPath, Status, TargetId, DateCreated, Progress from SyncJobItems"; public SyncJob GetJob(string id) { @@ -100,6 +116,11 @@ namespace MediaBrowser.Server.Implementations.Sync var guid = new Guid(id); + if (guid == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + using (var cmd = _connection.CreateCommand()) { cmd.CommandText = BaseJobSelectText + " where Id=@Id"; @@ -149,28 +170,34 @@ namespace MediaBrowser.Server.Implementations.Sync if (!reader.IsDBNull(7)) { - info.RequestedItemIds = reader.GetString(7).Split(',').ToList(); + info.RequestedItemIds = reader.GetString(7).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } if (!reader.IsDBNull(8)) { - info.UnwatchedOnly = reader.GetBoolean(8); + info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(8), true); } if (!reader.IsDBNull(9)) { - info.Limit = reader.GetInt64(9); + info.ParentId = reader.GetString(9); } if (!reader.IsDBNull(10)) { - info.LimitType = (SyncLimitType)Enum.Parse(typeof(SyncLimitType), reader.GetString(10), true); + info.UnwatchedOnly = reader.GetBoolean(10); + } + + if (!reader.IsDBNull(11)) + { + info.ItemLimit = reader.GetInt32(11); } - info.IsDynamic = reader.GetBoolean(11); - info.DateCreated = reader.GetDateTime(12).ToUniversalTime(); - info.DateLastModified = reader.GetDateTime(13).ToUniversalTime(); - info.ItemCount = reader.GetInt32(14); + info.SyncNewContent = reader.GetBoolean(12); + + info.DateCreated = reader.GetDateTime(13).ToUniversalTime(); + info.DateLastModified = reader.GetDateTime(14).ToUniversalTime(); + info.ItemCount = reader.GetInt32(15); return info; } @@ -201,14 +228,15 @@ namespace MediaBrowser.Server.Implementations.Sync _saveJobCommand.GetParameter(index++).Value = job.TargetId; _saveJobCommand.GetParameter(index++).Value = job.Name; _saveJobCommand.GetParameter(index++).Value = job.Quality; - _saveJobCommand.GetParameter(index++).Value = job.Status; + _saveJobCommand.GetParameter(index++).Value = job.Status.ToString(); _saveJobCommand.GetParameter(index++).Value = job.Progress; _saveJobCommand.GetParameter(index++).Value = job.UserId; _saveJobCommand.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); + _saveJobCommand.GetParameter(index++).Value = job.Category; + _saveJobCommand.GetParameter(index++).Value = job.ParentId; _saveJobCommand.GetParameter(index++).Value = job.UnwatchedOnly; - _saveJobCommand.GetParameter(index++).Value = job.Limit; - _saveJobCommand.GetParameter(index++).Value = job.LimitType; - _saveJobCommand.GetParameter(index++).Value = job.IsDynamic; + _saveJobCommand.GetParameter(index++).Value = job.ItemLimit; + _saveJobCommand.GetParameter(index++).Value = job.SyncNewContent; _saveJobCommand.GetParameter(index++).Value = job.DateCreated; _saveJobCommand.GetParameter(index++).Value = job.DateLastModified; _saveJobCommand.GetParameter(index++).Value = job.ItemCount; @@ -250,6 +278,65 @@ namespace MediaBrowser.Server.Implementations.Sync } } + public async Task DeleteJob(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + await _writeLock.WaitAsync().ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + var index = 0; + + _deleteJobCommand.GetParameter(index++).Value = new Guid(id); + _deleteJobCommand.Transaction = transaction; + _deleteJobCommand.ExecuteNonQuery(); + + index = 0; + _deleteJobItemsCommand.GetParameter(index++).Value = id; + _deleteJobItemsCommand.Transaction = transaction; + _deleteJobItemsCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + _logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + public QueryResult<SyncJob> GetJobs(SyncJobQuery query) { if (query == null) @@ -263,8 +350,34 @@ namespace MediaBrowser.Server.Implementations.Sync var whereClauses = new List<string>(); - var startIndex = query.StartIndex ?? 0; + if (query.IsCompleted.HasValue) + { + if (query.IsCompleted.Value) + { + whereClauses.Add("Status=@Status"); + } + else + { + whereClauses.Add("Status<>@Status"); + } + cmd.Parameters.Add(cmd, "@Status", DbType.String).Value = SyncJobStatus.Completed.ToString(); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + var startIndex = query.StartIndex ?? 0; if (startIndex > 0) { whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY DateLastModified DESC LIMIT {0})", @@ -283,7 +396,7 @@ namespace MediaBrowser.Server.Implementations.Sync cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); } - cmd.CommandText += "; select count (Id) from SyncJobs"; + cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; var list = new List<SyncJob>(); var count = 0; @@ -328,7 +441,7 @@ namespace MediaBrowser.Server.Implementations.Sync { if (reader.Read()) { - return GetSyncJobItem(reader); + return GetJobItem(reader); } } } @@ -336,6 +449,86 @@ namespace MediaBrowser.Server.Implementations.Sync return null; } + public QueryResult<SyncJobItem> GetJobItems(SyncJobItemQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = BaseJobItemSelectText; + + var whereClauses = new List<string>(); + + if (!string.IsNullOrWhiteSpace(query.JobId)) + { + whereClauses.Add("JobId=@JobId"); + cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + + if (query.Statuses.Count > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY DateCreated LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + cmd.CommandText += " ORDER BY DateCreated"; + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; + + var list = new List<SyncJobItem>(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + list.Add(GetJobItem(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult<SyncJobItem>() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + public Task Create(SyncJobItem jobItem) { return Update(jobItem); @@ -360,10 +553,14 @@ namespace MediaBrowser.Server.Implementations.Sync _saveJobItemCommand.GetParameter(index++).Value = new Guid(jobItem.Id); _saveJobItemCommand.GetParameter(index++).Value = jobItem.ItemId; + _saveJobItemCommand.GetParameter(index++).Value = jobItem.ItemName; + _saveJobItemCommand.GetParameter(index++).Value = jobItem.MediaSourceId; _saveJobItemCommand.GetParameter(index++).Value = jobItem.JobId; _saveJobItemCommand.GetParameter(index++).Value = jobItem.OutputPath; - _saveJobItemCommand.GetParameter(index++).Value = jobItem.Status; + _saveJobItemCommand.GetParameter(index++).Value = jobItem.Status.ToString(); _saveJobItemCommand.GetParameter(index++).Value = jobItem.TargetId; + _saveJobItemCommand.GetParameter(index++).Value = jobItem.DateCreated; + _saveJobItemCommand.GetParameter(index++).Value = jobItem.Progress; _saveJobItemCommand.Transaction = transaction; @@ -402,26 +599,44 @@ namespace MediaBrowser.Server.Implementations.Sync } } - private SyncJobItem GetSyncJobItem(IDataReader reader) + private SyncJobItem GetJobItem(IDataReader reader) { var info = new SyncJobItem { Id = reader.GetGuid(0).ToString("N"), - ItemId = reader.GetString(1), - JobId = reader.GetString(2) + ItemId = reader.GetString(1) }; + if (!reader.IsDBNull(2)) + { + info.ItemName = reader.GetString(2); + } + if (!reader.IsDBNull(3)) { - info.OutputPath = reader.GetString(3); + info.MediaSourceId = reader.GetString(3); } - if (!reader.IsDBNull(4)) + info.JobId = reader.GetString(4); + + if (!reader.IsDBNull(5)) { - info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(4), true); + info.OutputPath = reader.GetString(5); } - info.TargetId = reader.GetString(5); + if (!reader.IsDBNull(6)) + { + info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader.GetString(6), true); + } + + info.TargetId = reader.GetString(7); + + info.DateCreated = reader.GetDateTime(8); + + if (!reader.IsDBNull(9)) + { + info.Progress = reader.GetDouble(9); + } return info; } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncScheduledTask.cs b/MediaBrowser.Server.Implementations/Sync/SyncScheduledTask.cs new file mode 100644 index 0000000000..7971842987 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Sync/SyncScheduledTask.cs @@ -0,0 +1,78 @@ +using MediaBrowser.Common.ScheduledTasks; +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; + +namespace MediaBrowser.Server.Implementations.Sync +{ + public class SyncScheduledTask : IScheduledTask, IConfigurableScheduledTask + { + 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; + + public SyncScheduledTask(ILibraryManager libraryManager, ISyncRepository syncRepo, ISyncManager syncManager, ILogger logger, IUserManager userManager, ITVSeriesManager tvSeriesManager, IMediaEncoder mediaEncoder) + { + _libraryManager = libraryManager; + _syncRepo = syncRepo; + _syncManager = syncManager; + _logger = logger; + _userManager = userManager; + _tvSeriesManager = tvSeriesManager; + MediaEncoder = mediaEncoder; + } + + public string Name + { + get { return "Sync preparation"; } + } + + public string Description + { + get { return "Runs scheduled sync jobs"; } + } + + public string Category + { + get + { + return "Library"; + } + } + + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + { + return new SyncJobProcessor(_libraryManager, _syncRepo, _syncManager, _logger, _userManager, _tvSeriesManager, MediaEncoder).Sync(progress, + cancellationToken); + } + + public IEnumerable<ITaskTrigger> GetDefaultTriggers() + { + return new ITaskTrigger[] + { + new IntervalTrigger { Interval = TimeSpan.FromHours(3) }, + new StartupTrigger{ DelayMs = Convert.ToInt32(TimeSpan.FromMinutes(5).TotalMilliseconds)} + }; + } + + public bool IsHidden + { + get { return false; } + } + + public bool IsEnabled + { + get { return true; } + } + } +} |
