jellyfin-server/MediaBrowser.Server.Implementations/Sync/MediaSync.cs
2015-03-06 01:10:53 -05:00

369 lines
13 KiB
C#

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<double> progress,
CancellationToken cancellationToken)
{
var serverId = _appHost.SystemId;
await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false);
progress.Report(3);
var innerProgress = new ActionableProgress<double>();
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<double> 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<double>();
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<double> progress,
CancellationToken cancellationToken)
{
var libraryItem = jobItem.Item;
var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId);
var fileTransferProgress = new ActionableProgress<double>();
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<double>(), 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<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)
{
var path = item.LocalPath;
path = provider.GetParentDirectoryPath(path, target);
var list = await provider.GetFileSystemEntries(path, target).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;
}
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);
}
}
}