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; namespace MediaBrowser.Server.Implementations.Sync { public class MediaSync { private readonly ISyncManager _syncManager; private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem) { _logger = logger; _syncManager = syncManager; _appHost = appHost; _fileSystem = fileSystem; } public async Task Sync(IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, IProgress progress, CancellationToken cancellationToken) { var serverId = _appHost.SystemId; await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false); progress.Report(3); var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => { var totalProgress = pct * .97; totalProgress += 1; progress.Report(totalProgress); }); await GetNewMedia(provider, dataProvider, target, serverId, 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 dataProvider.GetServerItemIds(target, serverId).ConfigureAwait(false); var result = await _syncManager.SyncData(new SyncDataRequest { TargetId = target.Id, LocalItemIds = localIds }).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); foreach (var itemIdToRemove in result.ItemIdsToRemove) { try { await RemoveItem(provider, dataProvider, serverId, itemIdToRemove, target, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error deleting item from device. Id: {0}", ex, itemIdToRemove); } } } private async Task GetNewMedia(IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, string serverId, IProgress progress, CancellationToken cancellationToken) { var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false); var numComplete = 0; double startingPercent = 0; double percentPerItem = 1; if (jobItems.Count > 0) { percentPerItem /= jobItems.Count; } foreach (var jobItem in jobItems) { cancellationToken.ThrowIfCancellationRequested(); var currentPercent = startingPercent; var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(pct => { var totalProgress = pct * percentPerItem; totalProgress += currentPercent; progress.Report(totalProgress); }); await GetItem(provider, dataProvider, target, serverId, jobItem, innerProgress, cancellationToken).ConfigureAwait(false); numComplete++; startingPercent = numComplete; startingPercent /= jobItems.Count; startingPercent *= 100; progress.Report(startingPercent); } } private async Task GetItem(IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, string serverId, SyncedItem jobItem, IProgress progress, CancellationToken cancellationToken) { var libraryItem = jobItem.Item; var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId); var fileTransferProgress = new ActionableProgress(); fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92)); var localItem = CreateLocalItem(provider, jobItem.SyncJobId, target, libraryItem, serverId, jobItem.OriginalFileName); await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id); var transferSuccess = false; Exception transferException = null; try { await SendFile(provider, internalSyncJobItem.OutputPath, localItem, target, cancellationToken).ConfigureAwait(false); // Create db record await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false); progress.Report(92); transferSuccess = true; progress.Report(99); } catch (Exception ex) { _logger.ErrorException("Error transferring sync job file", ex); transferException = ex; } if (transferSuccess) { await _syncManager.ReportSyncJobItemTransferred(jobItem.SyncJobItemId).ConfigureAwait(false); } else { await _syncManager.ReportSyncJobItemTransferFailed(jobItem.SyncJobItemId).ConfigureAwait(false); throw transferException; } } private async Task RemoveItem(IServerSyncProvider provider, ISyncDataProvider dataProvider, string serverId, string itemId, SyncTarget target, CancellationToken cancellationToken) { var localId = GetLocalId(serverId, itemId); var localItem = await dataProvider.Get(target, localId); if (localItem == null) { return; } var files = await GetFiles(provider, localItem, target); foreach (var file in files) { await provider.DeleteFile(file.Path, target, cancellationToken).ConfigureAwait(false); } await dataProvider.Delete(target, localId).ConfigureAwait(false); } private Task SendFile(IServerSyncProvider provider, string inputPath, LocalItem item, SyncTarget target, CancellationToken cancellationToken) { return provider.SendFile(inputPath, item.LocalPath, target, new Progress(), cancellationToken); } private string GetLocalId(string serverId, string itemId) { var bytes = Encoding.UTF8.GetBytes(serverId + itemId); bytes = CreateMD5(bytes); return BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty); } private byte[] CreateMD5(byte[] value) { using (var provider = MD5.Create()) { return provider.ComputeHash(value); } } public LocalItem CreateLocalItem(IServerSyncProvider provider, string syncJobId, 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(serverId, libraryItem.Id) }; } private List GetDirectoryPath(IServerSyncProvider provider, string syncJobId, BaseItemDto item, string serverId) { var parts = new List { 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> GetFiles(IServerSyncProvider provider, LocalItem item, SyncTarget target) { var path = item.LocalPath; path = provider.GetParentDirectoryPath(path, target); var list = await provider.GetFileSystemEntries(path, target).ConfigureAwait(false); var itemFiles = new List(); 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; } else if (!IsImageFile(file.Name)) { itemFile.Type = ItemFileType.Media; } itemFiles.Add(itemFile); } return itemFiles; } private static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".webp" }; private bool IsImageFile(string path) { var ext = Path.GetExtension(path) ?? string.Empty; return SupportedImageExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase); } 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); } } }