jellyfin/MediaBrowser.Server.Implementations/Sync/MediaSync.cs

500 lines
18 KiB
C#
Raw Normal View History

2015-04-13 05:12:02 +00:00
using MediaBrowser.Common.Configuration;
2015-03-31 02:52:18 +00:00
using MediaBrowser.Common.IO;
2015-02-28 13:42:47 +00:00
using MediaBrowser.Common.Progress;
2015-02-05 05:29:37 +00:00
using MediaBrowser.Controller;
2015-04-05 15:01:57 +00:00
using MediaBrowser.Controller.IO;
2015-02-05 05:29:37 +00:00
using MediaBrowser.Controller.Sync;
2015-02-22 19:05:38 +00:00
using MediaBrowser.Model.Dto;
2015-02-28 13:42:47 +00:00
using MediaBrowser.Model.Entities;
2015-02-05 05:29:37 +00:00
using MediaBrowser.Model.Logging;
2015-02-28 13:42:47 +00:00
using MediaBrowser.Model.MediaInfo;
2015-02-05 05:29:37 +00:00
using MediaBrowser.Model.Sync;
using System;
2015-02-28 13:42:47 +00:00
using System.Collections.Generic;
2015-04-13 05:12:02 +00:00
using System.Globalization;
2015-03-27 20:55:31 +00:00
using System.IO;
2015-02-28 13:42:47 +00:00
using System.Linq;
using System.Security.Cryptography;
using System.Text;
2015-02-05 05:29:37 +00:00
using System.Threading;
using System.Threading.Tasks;
2015-10-04 04:23:11 +00:00
using CommonIO;
2015-04-15 03:41:29 +00:00
using Interfaces.IO;
2015-02-05 05:29:37 +00:00
namespace MediaBrowser.Server.Implementations.Sync
{
public class MediaSync
{
private readonly ISyncManager _syncManager;
private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
2015-02-28 13:42:47 +00:00
private readonly IFileSystem _fileSystem;
2015-04-05 15:01:57 +00:00
private readonly IConfigurationManager _config;
2015-02-05 05:29:37 +00:00
2015-04-13 05:12:02 +00:00
public const string PathSeparatorString = "/";
public const char PathSeparatorChar = '/';
2015-04-05 15:01:57 +00:00
public MediaSync(ILogger logger, ISyncManager syncManager, IServerApplicationHost appHost, IFileSystem fileSystem, IConfigurationManager config)
2015-02-05 05:29:37 +00:00
{
_logger = logger;
_syncManager = syncManager;
_appHost = appHost;
2015-02-28 13:42:47 +00:00
_fileSystem = fileSystem;
2015-04-05 15:01:57 +00:00
_config = config;
2015-02-05 05:29:37 +00:00
}
2015-02-26 20:06:42 +00:00
public async Task Sync(IServerSyncProvider provider,
2015-02-28 13:42:47 +00:00
ISyncDataProvider dataProvider,
2015-02-05 05:29:37 +00:00
SyncTarget target,
IProgress<double> progress,
CancellationToken cancellationToken)
{
var serverId = _appHost.SystemId;
2015-03-29 23:47:31 +00:00
var serverName = _appHost.FriendlyName;
2015-02-05 05:29:37 +00:00
2015-02-28 13:42:47 +00:00
await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false);
2015-02-05 05:29:37 +00:00
progress.Report(3);
var innerProgress = new ActionableProgress<double>();
innerProgress.RegisterAction(pct =>
{
var totalProgress = pct * .97;
totalProgress += 1;
progress.Report(totalProgress);
});
2015-03-29 23:47:31 +00:00
await GetNewMedia(provider, dataProvider, target, serverId, serverName, innerProgress, cancellationToken);
2015-02-22 19:05:38 +00:00
// Do the data sync twice so the server knows what was removed from the device
2015-02-28 13:42:47 +00:00
await SyncData(provider, dataProvider, serverId, target, cancellationToken).ConfigureAwait(false);
2015-11-06 15:02:22 +00:00
2015-02-05 05:29:37 +00:00
progress.Report(100);
}
private async Task SyncData(IServerSyncProvider provider,
2015-02-28 13:42:47 +00:00
ISyncDataProvider dataProvider,
2015-02-05 05:29:37 +00:00
string serverId,
SyncTarget target,
CancellationToken cancellationToken)
{
2015-04-13 05:12:02 +00:00
var localItems = await dataProvider.GetLocalItems(target, serverId).ConfigureAwait(false);
var remoteFiles = await provider.GetFiles(new FileQuery(), target, cancellationToken).ConfigureAwait(false);
var remoteIds = remoteFiles.Items.Select(i => i.Id).ToList();
var jobItemIds = new List<string>();
foreach (var localItem in localItems)
{
2015-10-15 02:55:19 +00:00
if (remoteIds.Contains(localItem.FileId, StringComparer.OrdinalIgnoreCase))
2015-04-13 05:12:02 +00:00
{
jobItemIds.Add(localItem.SyncJobItemId);
}
}
2015-02-26 20:06:42 +00:00
2015-02-28 13:42:47 +00:00
var result = await _syncManager.SyncData(new SyncDataRequest
{
TargetId = target.Id,
2015-03-31 18:50:08 +00:00
SyncJobItemIds = jobItemIds
2015-02-26 20:06:42 +00:00
2015-02-28 13:42:47 +00:00
}).ConfigureAwait(false);
2015-02-26 20:06:42 +00:00
2015-02-28 13:42:47 +00:00
cancellationToken.ThrowIfCancellationRequested();
2015-02-26 20:06:42 +00:00
2015-02-28 13:42:47 +00:00
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);
}
}
2015-02-05 05:29:37 +00:00
}
private async Task GetNewMedia(IServerSyncProvider provider,
2015-02-28 13:42:47 +00:00
ISyncDataProvider dataProvider,
2015-02-05 05:29:37 +00:00
SyncTarget target,
string serverId,
2015-03-29 23:47:31 +00:00
string serverName,
2015-02-05 05:29:37 +00:00
IProgress<double> progress,
CancellationToken cancellationToken)
{
2015-02-26 20:06:42 +00:00
var jobItems = await _syncManager.GetReadySyncItems(target.Id).ConfigureAwait(false);
2015-02-05 05:29:37 +00:00
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);
});
2015-09-04 16:20:54 +00:00
try
{
await GetItem(provider, dataProvider, target, serverId, serverName, jobItem, innerProgress, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error syncing item", ex);
}
2015-02-05 05:29:37 +00:00
numComplete++;
startingPercent = numComplete;
startingPercent /= jobItems.Count;
startingPercent *= 100;
progress.Report(startingPercent);
}
}
private async Task GetItem(IServerSyncProvider provider,
2015-02-28 13:42:47 +00:00
ISyncDataProvider dataProvider,
2015-02-05 05:29:37 +00:00
SyncTarget target,
string serverId,
2015-03-29 23:47:31 +00:00
string serverName,
2015-02-05 05:29:37 +00:00
SyncedItem jobItem,
IProgress<double> progress,
CancellationToken cancellationToken)
{
var libraryItem = jobItem.Item;
var internalSyncJobItem = _syncManager.GetJobItem(jobItem.SyncJobItemId);
2015-03-31 02:52:18 +00:00
var internalSyncJob = _syncManager.GetJob(jobItem.SyncJobId);
2015-02-05 05:29:37 +00:00
2015-03-31 02:52:18 +00:00
var localItem = CreateLocalItem(provider, jobItem, internalSyncJob, target, libraryItem, serverId, serverName, jobItem.OriginalFileName);
2015-02-28 13:42:47 +00:00
2015-02-05 05:29:37 +00:00
await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id);
var transferSuccess = false;
Exception transferException = null;
2015-04-05 15:01:57 +00:00
var options = _config.GetSyncOptions();
2015-02-05 05:29:37 +00:00
try
{
2015-03-27 20:55:31 +00:00
var fileTransferProgress = new ActionableProgress<double>();
fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92));
2015-04-13 05:12:02 +00:00
var sendFileResult = await SendFile(provider, internalSyncJobItem.OutputPath, localItem.LocalPath.Split(PathSeparatorChar), target, options, fileTransferProgress, cancellationToken).ConfigureAwait(false);
2015-03-13 05:29:39 +00:00
if (localItem.Item.MediaSources != null)
{
var mediaSource = localItem.Item.MediaSources.FirstOrDefault();
if (mediaSource != null)
{
mediaSource.Path = sendFileResult.Path;
mediaSource.Protocol = sendFileResult.Protocol;
2015-03-28 20:22:27 +00:00
mediaSource.RequiredHttpHeaders = sendFileResult.RequiredHttpHeaders;
2015-03-13 05:29:39 +00:00
mediaSource.SupportsTranscoding = false;
}
}
2015-02-22 19:05:38 +00:00
2015-04-13 05:12:02 +00:00
localItem.FileId = sendFileResult.Id;
2015-02-28 13:42:47 +00:00
// Create db record
await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false);
2015-02-05 05:29:37 +00:00
2015-03-25 22:45:25 +00:00
if (localItem.Item.MediaSources != null)
{
var mediaSource = localItem.Item.MediaSources.FirstOrDefault();
if (mediaSource != null)
{
2015-04-05 15:01:57 +00:00
await SendSubtitles(localItem, mediaSource, provider, dataProvider, target, options, cancellationToken).ConfigureAwait(false);
2015-03-25 22:45:25 +00:00
}
}
2015-02-05 05:29:37 +00:00
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;
}
}
2015-04-05 15:01:57 +00:00
private async Task SendSubtitles(LocalItem localItem, MediaSourceInfo mediaSource, IServerSyncProvider provider, ISyncDataProvider dataProvider, SyncTarget target, SyncOptions options, CancellationToken cancellationToken)
2015-03-25 22:45:25 +00:00
{
2015-03-26 23:10:34 +00:00
var failedSubtitles = new List<MediaStream>();
var requiresSave = false;
2015-03-25 22:45:25 +00:00
foreach (var mediaStream in mediaSource.MediaStreams
.Where(i => i.Type == MediaStreamType.Subtitle && i.IsExternal)
.ToList())
{
2015-03-26 23:10:34 +00:00
try
{
2015-03-27 04:17:04 +00:00
var remotePath = GetRemoteSubtitlePath(localItem, mediaStream, provider, target);
2015-04-05 15:01:57 +00:00
var sendFileResult = await SendFile(provider, mediaStream.Path, remotePath, target, options, new Progress<double>(), cancellationToken).ConfigureAwait(false);
2015-03-25 22:45:25 +00:00
2015-03-28 02:19:20 +00:00
// This is the path that will be used when talking to the provider
2015-04-13 05:12:02 +00:00
mediaStream.ExternalId = sendFileResult.Id;
2015-03-28 02:19:20 +00:00
// Keep track of all additional files for cleanup later.
2015-04-13 05:12:02 +00:00
localItem.AdditionalFiles.Add(sendFileResult.Id);
2015-03-28 02:19:20 +00:00
// This is the public path clients will use
2015-03-26 23:10:34 +00:00
mediaStream.Path = sendFileResult.Path;
requiresSave = true;
}
catch (Exception ex)
{
_logger.ErrorException("Error sending subtitle stream", ex);
failedSubtitles.Add(mediaStream);
}
}
if (failedSubtitles.Count > 0)
{
mediaSource.MediaStreams = mediaSource.MediaStreams.Except(failedSubtitles).ToList();
requiresSave = true;
}
if (requiresSave)
{
2015-03-25 22:45:25 +00:00
await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false);
2015-03-26 23:10:34 +00:00
}
2015-03-25 22:45:25 +00:00
}
2015-04-13 05:12:02 +00:00
private string[] GetRemoteSubtitlePath(LocalItem item, MediaStream stream, IServerSyncProvider provider, SyncTarget target)
2015-03-27 04:17:04 +00:00
{
var filename = GetSubtitleSaveFileName(item, stream.Language, stream.IsForced) + "." + stream.Codec.ToLower();
2015-04-13 05:12:02 +00:00
var pathParts = item.LocalPath.Split(PathSeparatorChar);
var list = pathParts.Take(pathParts.Length - 1).ToList();
list.Add(filename);
2015-03-27 04:17:04 +00:00
2015-04-13 05:12:02 +00:00
return list.ToArray();
2015-03-27 04:17:04 +00:00
}
private string GetSubtitleSaveFileName(LocalItem item, string language, bool isForced)
{
var path = item.LocalPath;
var name = Path.GetFileNameWithoutExtension(path);
if (!string.IsNullOrWhiteSpace(language))
{
name += "." + language.ToLower();
}
if (isForced)
{
name += ".foreign";
}
return name;
}
2015-02-28 13:42:47 +00:00
private async Task RemoveItem(IServerSyncProvider provider,
ISyncDataProvider dataProvider,
2015-02-05 05:29:37 +00:00
string serverId,
2015-03-31 18:50:08 +00:00
string syncJobItemId,
2015-02-05 05:29:37 +00:00
SyncTarget target,
CancellationToken cancellationToken)
{
2015-04-04 01:24:49 +00:00
var localItems = await dataProvider.GetItemsBySyncJobItemId(target, serverId, syncJobItemId);
2015-02-28 13:42:47 +00:00
2015-03-10 18:10:38 +00:00
foreach (var localItem in localItems)
2015-02-28 13:42:47 +00:00
{
2015-03-28 02:19:20 +00:00
var files = localItem.AdditionalFiles.ToList();
2015-04-13 05:12:02 +00:00
2015-03-10 18:10:38 +00:00
foreach (var file in files)
{
2015-03-28 02:19:20 +00:00
_logger.Debug("Removing {0} from {1}.", file, target.Name);
await provider.DeleteFile(file, target, cancellationToken).ConfigureAwait(false);
2015-03-10 18:10:38 +00:00
}
2015-02-28 13:42:47 +00:00
2015-03-10 18:10:38 +00:00
await dataProvider.Delete(target, localItem.Id).ConfigureAwait(false);
2015-02-28 13:42:47 +00:00
}
}
2015-04-13 05:12:02 +00:00
private async Task<SyncedFileInfo> SendFile(IServerSyncProvider provider, string inputPath, string[] pathParts, SyncTarget target, SyncOptions options, IProgress<double> progress, CancellationToken cancellationToken)
2015-02-28 13:42:47 +00:00
{
2015-04-13 05:12:02 +00:00
_logger.Debug("Sending {0} to {1}. Remote path: {2}", inputPath, provider.Name, string.Join("/", pathParts));
2015-07-06 07:06:09 +00:00
var supportsDirectCopy = provider as ISupportsDirectCopy;
if (supportsDirectCopy != null)
{
return await supportsDirectCopy.SendFile(inputPath, pathParts, target, progress, cancellationToken).ConfigureAwait(false);
}
2015-04-05 15:01:57 +00:00
using (var fileStream = _fileSystem.GetFileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read, true))
2015-03-08 04:44:31 +00:00
{
2015-04-05 15:01:57 +00:00
Stream stream = fileStream;
if (options.UploadSpeedLimitBytes > 0 && provider is IRemoteSyncProvider)
{
stream = new ThrottledStream(stream, options.UploadSpeedLimitBytes);
}
2015-04-13 05:12:02 +00:00
return await provider.SendFile(stream, pathParts, target, progress, cancellationToken).ConfigureAwait(false);
2015-03-08 04:44:31 +00:00
}
2015-02-28 13:42:47 +00:00
}
2015-03-10 18:10:38 +00:00
private static string GetLocalId(string jobItemId, string itemId)
2015-02-28 13:42:47 +00:00
{
2015-03-10 18:10:38 +00:00
var bytes = Encoding.UTF8.GetBytes(jobItemId + itemId);
2015-03-08 05:37:48 +00:00
bytes = CreateMd5(bytes);
2015-02-28 13:42:47 +00:00
return BitConverter.ToString(bytes, 0, bytes.Length).Replace("-", string.Empty);
}
2015-03-08 05:37:48 +00:00
private static byte[] CreateMd5(byte[] value)
2015-02-28 13:42:47 +00:00
{
using (var provider = MD5.Create())
{
return provider.ComputeHash(value);
}
2015-02-05 05:29:37 +00:00
}
2015-02-22 19:05:38 +00:00
2015-03-31 02:52:18 +00:00
public LocalItem CreateLocalItem(IServerSyncProvider provider, SyncedItem syncedItem, SyncJob job, SyncTarget target, BaseItemDto libraryItem, string serverId, string serverName, string originalFileName)
2015-02-22 19:05:38 +00:00
{
2015-03-31 02:52:18 +00:00
var path = GetDirectoryPath(provider, job, syncedItem, libraryItem, serverName);
2015-02-28 13:42:47 +00:00
path.Add(GetLocalFileName(provider, libraryItem, originalFileName));
2015-04-13 05:12:02 +00:00
var localPath = string.Join(PathSeparatorString, path.ToArray());
2015-02-28 13:42:47 +00:00
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,
2015-03-31 18:50:08 +00:00
Id = GetLocalId(syncedItem.SyncJobItemId, libraryItem.Id),
SyncJobItemId = syncedItem.SyncJobItemId
2015-02-28 13:42:47 +00:00
};
2015-02-22 19:05:38 +00:00
}
2015-02-26 20:06:42 +00:00
2015-03-31 02:52:18 +00:00
private List<string> GetDirectoryPath(IServerSyncProvider provider, SyncJob job, SyncedItem syncedItem, BaseItemDto item, string serverName)
2015-03-25 23:13:15 +00:00
{
2015-03-31 02:52:18 +00:00
var parts = new List<string>
{
serverName
};
2015-03-25 23:13:15 +00:00
2015-03-31 02:52:18 +00:00
var profileOption = _syncManager.GetProfileOptions(job.TargetId)
.FirstOrDefault(i => string.Equals(i.Id, job.Profile, StringComparison.OrdinalIgnoreCase));
2015-03-25 23:13:15 +00:00
2015-03-31 02:52:18 +00:00
string name;
2015-03-25 23:13:15 +00:00
2015-03-31 02:52:18 +00:00
if (profileOption != null && !string.IsNullOrWhiteSpace(profileOption.Name))
2015-02-28 13:42:47 +00:00
{
2015-03-31 02:52:18 +00:00
name = profileOption.Name;
if (job.Bitrate.HasValue)
{
name += "-" + job.Bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
else
{
var qualityOption = _syncManager.GetQualityOptions(job.TargetId)
.FirstOrDefault(i => string.Equals(i.Id, job.Quality, StringComparison.OrdinalIgnoreCase));
if (qualityOption != null && !string.IsNullOrWhiteSpace(qualityOption.Name))
{
name += "-" + qualityOption.Name;
}
}
}
else
{
name = syncedItem.SyncJobName + "-" + syncedItem.SyncJobDateCreated
.ToLocalTime()
.ToString("g")
.Replace(" ", "-");
}
name = GetValidFilename(provider, name);
parts.Add(name);
2015-02-28 13:42:47 +00:00
if (item.IsType("episode"))
{
2015-03-31 02:52:18 +00:00
parts.Add("TV");
if (!string.IsNullOrWhiteSpace(item.SeriesName))
2015-02-28 13:42:47 +00:00
{
parts.Add(item.SeriesName);
2015-02-28 13:42:47 +00:00
}
}
else if (item.IsVideo)
{
2015-03-31 02:52:18 +00:00
parts.Add("Videos");
2015-02-28 13:42:47 +00:00
parts.Add(item.Name);
}
else if (item.IsAudio)
{
2015-03-31 02:52:18 +00:00
parts.Add("Music");
2015-02-28 13:42:47 +00:00
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))
{
2015-03-31 02:52:18 +00:00
parts.Add("Photos");
2015-02-28 13:42:47 +00:00
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))
2015-02-28 13:42:47 +00:00
{
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);
}
2015-02-05 05:29:37 +00:00
}
}