Merge pull request #1 from MediaBrowser/dev

Dev
This commit is contained in:
jluce50 2015-03-03 09:42:17 -06:00
commit 6c2e01830c
96 changed files with 1624 additions and 475 deletions

View File

@ -318,7 +318,7 @@ namespace MediaBrowser.Api.Images
try
{
var size = _imageProcessor.GetImageSize(info.Path, info.DateModified);
var size = _imageProcessor.GetImageSize(info);
width = Convert.ToInt32(size.Width);
height = Convert.ToInt32(size.Height);

View File

@ -1,5 +1,4 @@
using MediaBrowser.Controller.Activity;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -9,11 +8,9 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Querying;
using ServiceStack;
using System;

View File

@ -81,6 +81,7 @@
<Compile Include="Library\ChapterService.cs" />
<Compile Include="Playback\Hls\MpegDashService.cs" />
<Compile Include="Playback\MediaInfoService.cs" />
<Compile Include="Playback\TranscodingThrottler.cs" />
<Compile Include="PlaylistService.cs" />
<Compile Include="Reports\ReportFieldType.cs" />
<Compile Include="Reports\ReportResult.cs" />

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
@ -70,12 +71,14 @@ namespace MediaBrowser.Api.Playback
protected IDeviceManager DeviceManager { get; private set; }
protected IChannelManager ChannelManager { get; private set; }
protected ISubtitleEncoder SubtitleEncoder { get; private set; }
protected IProcessManager ProcessManager { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager)
protected BaseStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager)
{
ProcessManager = processManager;
DeviceManager = deviceManager;
SubtitleEncoder = subtitleEncoder;
ChannelManager = channelManager;
@ -877,14 +880,6 @@ namespace MediaBrowser.Api.Playback
return "copy";
}
private bool SupportsThrottleWithStream
{
get
{
return false;
}
}
/// <summary>
/// Gets the input argument.
/// </summary>
@ -908,23 +903,15 @@ namespace MediaBrowser.Api.Playback
private string GetInputPathArgument(string transcodingJobId, StreamState state)
{
if (state.InputProtocol == MediaProtocol.File &&
state.RunTimeTicks.HasValue &&
state.VideoType == VideoType.VideoFile &&
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
{
if (SupportsThrottleWithStream)
{
var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId;
url += "&transcodingJobId=" + transcodingJobId;
return string.Format("\"{0}\"", url);
}
}
}
//if (state.InputProtocol == MediaProtocol.File &&
// state.RunTimeTicks.HasValue &&
// state.VideoType == VideoType.VideoFile &&
// !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
//{
// if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
// {
// }
//}
var protocol = state.InputProtocol;
@ -1109,9 +1096,26 @@ namespace MediaBrowser.Api.Playback
}
}
StartThrottler(state, transcodingJob);
return transcodingJob;
}
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
{
if (state.InputProtocol == MediaProtocol.File &&
state.RunTimeTicks.HasValue &&
state.VideoType == VideoType.VideoFile &&
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo)
{
state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ProcessManager);
state.TranscodingThrottler.Start();
}
}
}
private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
{
try

View File

@ -2,6 +2,7 @@
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@ -23,7 +24,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
public abstract class BaseHlsService : BaseStreamingService
{
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager)
{
}

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
@ -63,7 +64,7 @@ namespace MediaBrowser.Api.Playback.Hls
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager)
{
NetworkManager = networkManager;
}
@ -115,6 +116,7 @@ namespace MediaBrowser.Api.Playback.Hls
if (File.Exists(segmentPath))
{
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
}
@ -123,6 +125,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
if (File.Exists(segmentPath))
{
job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType);
return await GetSegmentResult(playlistPath, segmentPath, index, segmentLength, job, cancellationToken).ConfigureAwait(false);
}
else

View File

@ -3,6 +3,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@ -51,7 +52,7 @@ namespace MediaBrowser.Api.Playback.Hls
public class MpegDashService : BaseHlsService
{
public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager)
{
NetworkManager = networkManager;
}

View File

@ -2,6 +2,7 @@ using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@ -57,7 +58,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
public class VideoHlsService : BaseHlsService
{
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager)
{
}

View File

@ -13,6 +13,7 @@ using System.Threading.Tasks;
namespace MediaBrowser.Api.Playback
{
[Route("/Items/{Id}/MediaInfo", "GET", Summary = "Gets live playback media info for an item")]
[Route("/Items/{Id}/PlaybackInfo", "GET", Summary = "Gets live playback media info for an item")]
public class GetLiveMediaInfo : IReturn<LiveMediaInfoResult>
{
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]

View File

@ -3,6 +3,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@ -32,7 +33,7 @@ namespace MediaBrowser.Api.Playback.Progressive
/// </summary>
public class AudioService : BaseProgressiveStreamingService
{
public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager, imageProcessor, httpClient)
{
}

View File

@ -3,6 +3,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@ -29,7 +30,7 @@ namespace MediaBrowser.Api.Playback.Progressive
protected readonly IImageProcessor ImageProcessor;
protected readonly IHttpClient HttpClient;
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager)
protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager)
{
ImageProcessor = imageProcessor;
HttpClient = httpClient;
@ -153,49 +154,12 @@ namespace MediaBrowser.Api.Playback.Progressive
using (state)
{
var job = string.IsNullOrEmpty(request.TranscodingJobId) ?
null :
ApiEntryPoint.Instance.GetTranscodingJob(request.TranscodingJobId);
var limits = new List<long>();
if (state.InputBitrate.HasValue)
{
// Bytes per second
limits.Add((state.InputBitrate.Value / 8));
}
if (state.InputFileSize.HasValue && state.RunTimeTicks.HasValue)
{
var totalSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds;
if (totalSeconds > 1)
{
var timeBasedLimit = state.InputFileSize.Value / totalSeconds;
limits.Add(Convert.ToInt64(timeBasedLimit));
}
}
// Take the greater of the above to methods, just to be safe
var throttleLimit = limits.Count > 0 ? limits.First() : 0;
// Pad to play it safe
var bytesPerSecond = Convert.ToInt64(1.05 * throttleLimit);
// Don't even start evaluating this until at least two minutes have content have been consumed
var targetGap = throttleLimit * 120;
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
ResponseHeaders = responseHeaders,
ContentType = contentType,
IsHeadRequest = isHeadRequest,
Path = state.MediaPath,
Throttle = request.Throttle,
ThrottleLimit = bytesPerSecond,
MinThrottlePosition = targetGap,
ThrottleCallback = (l1, l2) => ThrottleCallack(l1, l2, bytesPerSecond, job)
Path = state.MediaPath
});
}
}
@ -234,67 +198,6 @@ namespace MediaBrowser.Api.Playback.Progressive
}
}
private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(3).Ticks;
private long ThrottleCallack(long currentBytesPerSecond, long bytesWritten, long originalBytesPerSecond, TranscodingJob job)
{
var bytesDownloaded = job.BytesDownloaded ?? 0;
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
var path = job.Path;
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
{
// Progressive Streaming - byte-based consideration
try
{
var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
// Estimate the bytes the transcoder should be ahead
double gapFactor = _gapLengthInTicks;
gapFactor /= transcodingPositionTicks;
var targetGap = bytesTranscoded * gapFactor;
var gap = bytesTranscoded - bytesDownloaded;
if (gap < targetGap)
{
//Logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return 0;
}
//Logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
}
catch
{
//Logger.Error("Error getting output size");
}
}
else if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
{
// HLS - time-based consideration
var targetGap = _gapLengthInTicks;
var gap = transcodingPositionTicks - downloadPositionTicks;
if (gap < targetGap)
{
//Logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
return 0;
}
//Logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
}
else
{
//Logger.Debug("No throttle data for " + path);
}
return originalBytesPerSecond;
}
/// <summary>
/// Gets the static remote stream result.
/// </summary>

View File

@ -3,6 +3,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
@ -63,7 +64,7 @@ namespace MediaBrowser.Api.Playback.Progressive
/// </summary>
public class VideoService : BaseProgressiveStreamingService
{
public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, imageProcessor, httpClient)
public VideoService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IProcessManager processManager, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder, deviceManager, processManager, imageProcessor, httpClient)
{
}

View File

@ -72,7 +72,6 @@ namespace MediaBrowser.Api.Playback
public string Params { get; set; }
public string ClientTime { get; set; }
public bool Throttle { get; set; }
public string TranscodingJobId { get; set; }
}

View File

@ -1,17 +1,16 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using MediaBrowser.Model.Net;
namespace MediaBrowser.Api.Playback
{
@ -23,6 +22,7 @@ namespace MediaBrowser.Api.Playback
public string RequestedUrl { get; set; }
public StreamRequest Request { get; set; }
public TranscodingThrottler TranscodingThrottler { get; set; }
public VideoStreamRequest VideoRequest
{
@ -125,6 +125,7 @@ namespace MediaBrowser.Api.Playback
public void Dispose()
{
DisposeTranscodingThrottler();
DisposeLiveStream();
DisposeLogStream();
DisposeIsoMount();
@ -147,6 +148,23 @@ namespace MediaBrowser.Api.Playback
}
}
private void DisposeTranscodingThrottler()
{
if (TranscodingThrottler != null)
{
try
{
TranscodingThrottler.Dispose();
}
catch (Exception ex)
{
_logger.ErrorException("Error disposing TranscodingThrottler", ex);
}
TranscodingThrottler = null;
}
}
private void DisposeIsoMount()
{
if (IsoMount != null)

View File

@ -0,0 +1,164 @@
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Model.Logging;
using System;
using System.IO;
using System.Threading;
namespace MediaBrowser.Api.Playback
{
public class TranscodingThrottler : IDisposable
{
private readonly TranscodingJob _job;
private readonly ILogger _logger;
private readonly IProcessManager _processManager;
private Timer _timer;
private bool _isPaused;
public void Start()
{
if (_processManager.SupportsSuspension)
{
_timer = new Timer(TimerCallback, null, 5000, 5000);
}
}
private void TimerCallback(object state)
{
if (_job.HasExited)
{
DisposeTimer();
return;
}
if (IsThrottleAllowed(_job))
{
PauseTranscoding();
}
else
{
UnpauseTranscoding();
}
}
private void PauseTranscoding()
{
if (!_isPaused)
{
_logger.Debug("Sending pause command to ffmpeg");
}
try
{
//_job.Process.StandardInput.WriteLine("p");
_processManager.SuspendProcess(_job.Process);
_isPaused = true;
}
catch (Exception ex)
{
_logger.ErrorException("Error pausing transcoding", ex);
}
}
private void UnpauseTranscoding()
{
if (_isPaused)
{
_logger.Debug("Sending unpause command to ffmpeg");
}
try
{
//_job.Process.StandardInput.WriteLine("u");
_processManager.ResumeProcess(_job.Process);
_isPaused = false;
}
catch (Exception ex)
{
_logger.ErrorException("Error unpausing transcoding", ex);
}
}
private readonly long _gapLengthInTicks = TimeSpan.FromMinutes(2).Ticks;
public TranscodingThrottler(TranscodingJob job, ILogger logger, IProcessManager processManager)
{
_job = job;
_logger = logger;
_processManager = processManager;
}
private bool IsThrottleAllowed(TranscodingJob job)
{
var bytesDownloaded = job.BytesDownloaded ?? 0;
var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;
var downloadPositionTicks = job.DownloadPositionTicks ?? 0;
var path = job.Path;
if (downloadPositionTicks > 0 && transcodingPositionTicks > 0)
{
// HLS - time-based consideration
var targetGap = _gapLengthInTicks;
var gap = transcodingPositionTicks - downloadPositionTicks;
if (gap < targetGap)
{
//_logger.Debug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap);
return false;
}
//_logger.Debug("Throttling transcoder gap {0} target gap {1}", gap, targetGap);
return true;
}
if (bytesDownloaded > 0 && transcodingPositionTicks > 0)
{
// Progressive Streaming - byte-based consideration
try
{
var bytesTranscoded = job.BytesTranscoded ?? new FileInfo(path).Length;
// Estimate the bytes the transcoder should be ahead
double gapFactor = _gapLengthInTicks;
gapFactor /= transcodingPositionTicks;
var targetGap = bytesTranscoded * gapFactor;
var gap = bytesTranscoded - bytesDownloaded;
if (gap < targetGap)
{
//_logger.Debug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return false;
}
//_logger.Debug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded);
return true;
}
catch
{
//_logger.Error("Error getting output size");
return false;
}
}
//_logger.Debug("No throttle data for " + path);
return false;
}
public void Dispose()
{
DisposeTimer();
}
private void DisposeTimer()
{
if (_timer != null)
{
_timer.Dispose();
_timer = null;
}
}
}
}

View File

@ -0,0 +1,28 @@
using System.Diagnostics;
namespace MediaBrowser.Controller.Diagnostics
{
/// <summary>
/// Interface IProcessManager
/// </summary>
public interface IProcessManager
{
/// <summary>
/// Gets a value indicating whether [supports suspension].
/// </summary>
/// <value><c>true</c> if [supports suspension]; otherwise, <c>false</c>.</value>
bool SupportsSuspension { get; }
/// <summary>
/// Suspends the process.
/// </summary>
/// <param name="process">The process.</param>
void SuspendProcess(Process process);
/// <summary>
/// Resumes the process.
/// </summary>
/// <param name="process">The process.</param>
void ResumeProcess(Process process);
}
}

View File

@ -2,7 +2,6 @@
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@ -30,10 +29,9 @@ namespace MediaBrowser.Controller.Drawing
/// <summary>
/// Gets the size of the image.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="imageDateModified">The image date modified.</param>
/// <param name="info">The information.</param>
/// <returns>ImageSize.</returns>
ImageSize GetImageSize(string path, DateTime imageDateModified);
ImageSize GetImageSize(ItemImageInfo info);
/// <summary>
/// Adds the parts.

View File

@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities.Audio
{
Id = i.Id.ToString("N"),
Protocol = locationType == LocationType.Remote ? MediaProtocol.Http : MediaProtocol.File,
MediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(),
MediaStreams = MediaSourceManager.GetMediaStreams(i.Id).ToList(),
Name = i.Name,
Path = enablePathSubstituion ? GetMappedPath(i.Path, locationType) : i.Path,
RunTimeTicks = i.RunTimeTicks,

View File

@ -420,12 +420,17 @@ namespace MediaBrowser.Controller.Entities
return base.GetDeletePaths();
}
public virtual IEnumerable<MediaStream> GetMediaStreams()
public IEnumerable<MediaStream> GetMediaStreams()
{
return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
var mediaSource = GetMediaSources(false)
.FirstOrDefault();
if (mediaSource == null)
{
ItemId = Id
});
return new List<MediaStream>();
}
return mediaSource.MediaStreams;
}
public virtual MediaStream GetDefaultVideoStream()
@ -474,7 +479,7 @@ namespace MediaBrowser.Controller.Entities
private static MediaSourceInfo GetVersionInfo(bool enablePathSubstitution, Video i, MediaSourceType type)
{
var mediaStreams = MediaSourceManager.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id })
var mediaStreams = MediaSourceManager.GetMediaStreams(i.Id)
.ToList();
var locationType = i.LocationType;

View File

@ -1,11 +1,29 @@
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
namespace MediaBrowser.Controller.Library
{
public interface IMediaSourceManager
{
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
IEnumerable<MediaStream> GetMediaStreams(Guid itemId);
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="mediaSourceId">The media source identifier.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId);
/// <summary>
/// Gets the media streams.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
IEnumerable<MediaStream> GetMediaStreams(MediaStreamQuery query);
}
}

View File

@ -34,6 +34,7 @@ namespace MediaBrowser.Controller.Library
event EventHandler<GenericEventArgs<User>> UserCreated;
event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
event EventHandler<GenericEventArgs<User>> UserLockedOut;
/// <summary>
/// Gets a User by Id

View File

@ -104,6 +104,7 @@
<Compile Include="Devices\CameraImageUploadInfo.cs" />
<Compile Include="Devices\IDeviceManager.cs" />
<Compile Include="Devices\IDeviceRepository.cs" />
<Compile Include="Diagnostics\IProcessManager.cs" />
<Compile Include="Dlna\ControlRequest.cs" />
<Compile Include="Dlna\ControlResponse.cs" />
<Compile Include="Dlna\EventSubscriptionResponse.cs" />
@ -341,8 +342,8 @@
<Compile Include="Subtitles\SubtitleDownloadEventArgs.cs" />
<Compile Include="Subtitles\SubtitleResponse.cs" />
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
<Compile Include="Sync\ICloudSyncProvider.cs" />
<Compile Include="Sync\IServerSyncProvider.cs" />
<Compile Include="Sync\ISyncDataProvider.cs" />
<Compile Include="Sync\ISyncManager.cs" />
<Compile Include="Sync\ISyncProvider.cs" />
<Compile Include="Sync\ISyncRepository.cs" />

View File

@ -18,11 +18,6 @@ namespace MediaBrowser.Controller.Net
public IDictionary<string, string> ResponseHeaders { get; set; }
public bool Throttle { get; set; }
public long ThrottleLimit { get; set; }
public long MinThrottlePosition { get; set; }
public Func<long, long, long> ThrottleCallback { get; set; }
public Action OnComplete { get; set; }
public StaticResultOptions()

View File

@ -1,35 +0,0 @@
using MediaBrowser.Model.Sync;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Sync
{
public interface ICloudSyncProvider
{
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets the synchronize targets.
/// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
IEnumerable<SyncTarget> GetSyncTargets(string userId);
/// <summary>
/// Transfers the item file.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="itemId">The item identifier.</param>
/// <param name="inputFile">The input file.</param>
/// <param name="pathParts">The path parts.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task TransferItemFile(string serverId, string itemId, string inputFile, string[] pathParts, SyncTarget target, CancellationToken cancellationToken);
}
}

View File

@ -1,5 +1,7 @@
using MediaBrowser.Model.Sync;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -7,35 +9,64 @@ namespace MediaBrowser.Controller.Sync
{
public interface IServerSyncProvider : ISyncProvider
{
/// <summary>
/// Gets the server item ids.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
Task<List<string>> GetServerItemIds(string serverId, SyncTarget target, CancellationToken cancellationToken);
/// <summary>
/// Removes the item.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="itemId">The item identifier.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task DeleteItem(string serverId, string itemId, SyncTarget target, CancellationToken cancellationToken);
/// <summary>
/// Transfers the file.
/// </summary>
/// <param name="serverId">The server identifier.</param>
/// <param name="itemId">The item identifier.</param>
/// <param name="inputFile">The input file.</param>
/// <param name="pathParts">The path parts.</param>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendFile(string inputFile, string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Deletes the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task TransferItemFile(string serverId, string itemId, string inputFile, string[] pathParts, SyncTarget target, CancellationToken cancellationToken);
Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken);
/// <summary>
/// Gets the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;Stream&gt;.</returns>
Task<Stream> GetFile(string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Gets the full path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <returns>System.String.</returns>
string GetFullPath(IEnumerable<string> path, SyncTarget target);
/// <summary>
/// Gets the parent directory path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <returns>System.String.</returns>
string GetParentDirectoryPath(string path, SyncTarget target);
/// <summary>
/// Gets the file system entries.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <returns>Task&lt;List&lt;DeviceFileInfo&gt;&gt;.</returns>
Task<List<DeviceFileInfo>> GetFileSystemEntries(string path, SyncTarget target);
/// <summary>
/// Gets the data provider.
/// </summary>
/// <returns>ISyncDataProvider.</returns>
ISyncDataProvider GetDataProvider();
}
}

View File

@ -0,0 +1,41 @@
using MediaBrowser.Model.Sync;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Sync
{
public interface ISyncDataProvider
{
/// <summary>
/// Gets the server item ids.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="serverId">The server identifier.</param>
/// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
/// <summary>
/// Adds the or update.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="item">The item.</param>
/// <returns>Task.</returns>
Task AddOrUpdate(SyncTarget target, LocalItem item);
/// <summary>
/// Deletes the specified identifier.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="id">The identifier.</param>
/// <returns>Task.</returns>
Task Delete(SyncTarget target, string id);
/// <summary>
/// Gets the specified identifier.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="id">The identifier.</param>
/// <returns>Task&lt;LocalItem&gt;.</returns>
Task<LocalItem> Get(SyncTarget target, string id);
}
}

View File

@ -1,5 +1,4 @@
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Sync;
using MediaBrowser.Model.Sync;
using System.Collections.Generic;
namespace MediaBrowser.Controller.Sync
@ -20,11 +19,10 @@ namespace MediaBrowser.Controller.Sync
IEnumerable<SyncTarget> GetSyncTargets(string userId);
/// <summary>
/// Gets the device profile.
/// Gets all synchronize targets.
/// </summary>
/// <param name="target">The target.</param>
/// <returns>DeviceProfile.</returns>
DeviceProfile GetDeviceProfile(SyncTarget target);
/// <returns>IEnumerable&lt;SyncTarget&gt;.</returns>
IEnumerable<SyncTarget> GetAllSyncTargets();
}
public interface IHasUniqueTargetIds

View File

@ -930,7 +930,7 @@ namespace MediaBrowser.Dlna.Didl
try
{
var size = _imageProcessor.GetImageSize(imageInfo.Path, imageInfo.DateModified);
var size = _imageProcessor.GetImageSize(imageInfo);
width = Convert.ToInt32(size.Width);
height = Convert.ToInt32(size.Height);

View File

@ -452,24 +452,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string GetInputPathArgument(EncodingJob job)
{
//if (job.InputProtocol == MediaProtocol.File &&
// job.RunTimeTicks.HasValue &&
// job.VideoType == VideoType.VideoFile &&
// !string.Equals(job.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
//{
// if (job.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && job.IsInputVideo)
// {
// if (SupportsThrottleWithStream)
// {
// var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/videos/" + job.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + job.Request.MediaSourceId;
// url += "&transcodingJobId=" + transcodingJobId;
// return string.Format("\"{0}\"", url);
// }
// }
//}
var protocol = job.InputProtocol;
var inputPath = new[] { job.MediaPath };

View File

@ -773,7 +773,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
try
{
using (var file = new FileStream(path, FileMode.Open))
using (var file = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var detector = new CharsetDetector();
detector.Feed(file);
@ -797,12 +797,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return null;
}
private static Encoding GetFileEncoding(string srcFile)
private Encoding GetFileEncoding(string srcFile)
{
// *** Detect byte order mark if any - otherwise assume default
var buffer = new byte[5];
using (var file = new FileStream(srcFile, FileMode.Open))
using (var file = _fileSystem.GetFileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
file.Read(buffer, 0, 5);
}

View File

@ -362,6 +362,12 @@
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs">
<Link>Dlna\MediaFormatProfileResolver.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs">
<Link>Dlna\PlaybackErrorCode.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackException.cs">
<Link>Dlna\PlaybackException.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\ProfileCondition.cs">
<Link>Dlna\ProfileCondition.cs</Link>
</Compile>

View File

@ -327,6 +327,12 @@
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs">
<Link>Dlna\MediaFormatProfileResolver.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs">
<Link>Dlna\PlaybackErrorCode.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackException.cs">
<Link>Dlna\PlaybackException.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\ProfileCondition.cs">
<Link>Dlna\ProfileCondition.cs</Link>
</Compile>

View File

@ -13,11 +13,17 @@ namespace MediaBrowser.Model.ApiClient
/// </summary>
/// <value><c>true</c> if [report capabilities]; otherwise, <c>false</c>.</value>
public bool ReportCapabilities { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [update date last accessed].
/// </summary>
/// <value><c>true</c> if [update date last accessed]; otherwise, <c>false</c>.</value>
public bool UpdateDateLastAccessed { get; set; }
public ConnectionOptions()
{
EnableWebSocket = true;
ReportCapabilities = true;
UpdateDateLastAccessed = true;
}
}
}

View File

@ -33,20 +33,12 @@ namespace MediaBrowser.Model.Configuration
public bool DisplayMissingEpisodes { get; set; }
public bool DisplayUnairedEpisodes { get; set; }
public bool EnableLiveTvManagement { get; set; }
public bool EnableLiveTvAccess { get; set; }
public bool EnableMediaPlayback { get; set; }
public bool EnableContentDeletion { get; set; }
public bool GroupMoviesIntoBoxSets { get; set; }
public string[] DisplayChannelsWithinViews { get; set; }
public string[] ExcludeFoldersFromGrouping { get; set; }
public UnratedItem[] BlockUnratedItems { get; set; }
public SubtitlePlaybackMode SubtitleMode { get; set; }
public bool DisplayCollectionsView { get; set; }
public bool DisplayFoldersView { get; set; }
@ -69,14 +61,10 @@ namespace MediaBrowser.Model.Configuration
public UserConfiguration()
{
PlayDefaultAudioTrack = true;
EnableLiveTvManagement = true;
EnableMediaPlayback = true;
EnableLiveTvAccess = true;
LatestItemsExcludes = new string[] { };
OrderedViews = new string[] { };
DisplayChannelsWithinViews = new string[] { };
BlockUnratedItems = new UnratedItem[] { };
ExcludeFoldersFromGrouping = new string[] { };
DisplayCollectionsView = true;

View File

@ -0,0 +1,10 @@

namespace MediaBrowser.Model.Dlna
{
public enum PlaybackErrorCode
{
NotAllowed = 0,
NoCompatibleStream = 1,
RateLimitExceeded = 2
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace MediaBrowser.Model.Dlna
{
public class PlaybackException : Exception
{
public PlaybackErrorCode ErrorCode { get; set;}
}
}

View File

@ -31,7 +31,13 @@ namespace MediaBrowser.Model.Dlna
List<StreamInfo> streams = new List<StreamInfo>();
foreach (MediaSourceInfo i in mediaSources)
streams.Add(BuildAudioItem(i, options));
{
StreamInfo streamInfo = BuildAudioItem(i, options);
if (streamInfo != null)
{
streams.Add(streamInfo);
}
}
foreach (StreamInfo stream in streams)
{
@ -63,7 +69,13 @@ namespace MediaBrowser.Model.Dlna
List<StreamInfo> streams = new List<StreamInfo>();
foreach (MediaSourceInfo i in mediaSources)
streams.Add(BuildVideoItem(i, options));
{
StreamInfo streamInfo = BuildVideoItem(i, options);
if (streamInfo != null)
{
streams.Add(streamInfo);
}
}
foreach (StreamInfo stream in streams)
{
@ -97,7 +109,10 @@ namespace MediaBrowser.Model.Dlna
{
return stream;
}
return null;
PlaybackException error = new PlaybackException();
error.ErrorCode = PlaybackErrorCode.NoCompatibleStream;
throw error;
}
private StreamInfo BuildAudioItem(MediaSourceInfo item, AudioOptions options)
@ -186,6 +201,11 @@ namespace MediaBrowser.Model.Dlna
if (transcodingProfile != null)
{
if (!item.SupportsTranscoding)
{
return null;
}
playlistItem.PlayMethod = PlayMethod.Transcode;
playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
playlistItem.EstimateContentLength = transcodingProfile.EstimateContentLength;
@ -267,7 +287,7 @@ namespace MediaBrowser.Model.Dlna
if (subtitleStream != null)
{
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile);
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile, options.Context);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
@ -290,9 +310,14 @@ namespace MediaBrowser.Model.Dlna
if (transcodingProfile != null)
{
if (!item.SupportsTranscoding)
{
return null;
}
if (subtitleStream != null)
{
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile);
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile, options.Context);
playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
playlistItem.SubtitleFormat = subtitleProfile.Format;
@ -527,7 +552,7 @@ namespace MediaBrowser.Model.Dlna
{
if (subtitleStream != null)
{
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile);
SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile, options.Context);
if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
{
@ -538,14 +563,20 @@ namespace MediaBrowser.Model.Dlna
return IsAudioEligibleForDirectPlay(item, maxBitrate);
}
public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, DeviceProfile deviceProfile)
public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, DeviceProfile deviceProfile, EncodingContext context)
{
// Look for an external profile that matches the stream type (text/graphical)
foreach (SubtitleProfile profile in deviceProfile.SubtitleProfiles)
{
if (subtitleStream.SupportsExternalStream)
if (profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
{
if (profile.Method == SubtitleDeliveryMethod.External && subtitleStream.IsTextSubtitleStream == MediaStream.IsTextFormat(profile.Format))
if (subtitleStream.SupportsExternalStream)
{
return profile;
}
// For sync we can handle the longer extraction times
if (context == EncodingContext.Static && subtitleStream.IsTextSubtitleStream)
{
return profile;
}

View File

@ -262,7 +262,7 @@ namespace MediaBrowser.Model.Dlna
private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream)
{
SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, DeviceProfile);
SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, DeviceProfile, Context);
if (subtitleProfile.Method != SubtitleDeliveryMethod.External)
{

View File

@ -22,6 +22,7 @@ namespace MediaBrowser.Model.Dto
public long? RunTimeTicks { get; set; }
public bool ReadAtNativeFramerate { get; set; }
public bool SupportsTranscoding { get; set; }
public VideoType? VideoType { get; set; }
@ -45,6 +46,7 @@ namespace MediaBrowser.Model.Dto
MediaStreams = new List<MediaStream>();
RequiredHttpHeaders = new Dictionary<string, string>();
PlayableStreamFileNames = new List<string>();
SupportsTranscoding = true;
}
public int? DefaultAudioStreamIndex { get; set; }

View File

@ -125,6 +125,8 @@
<Compile Include="Devices\DeviceInfo.cs" />
<Compile Include="Devices\DevicesOptions.cs" />
<Compile Include="Dlna\EncodingContext.cs" />
<Compile Include="Dlna\PlaybackErrorCode.cs" />
<Compile Include="Dlna\PlaybackException.cs" />
<Compile Include="Dlna\Profiles\DefaultProfile.cs" />
<Compile Include="Dlna\ResolutionConfiguration.cs" />
<Compile Include="Dlna\ResolutionNormalizer.cs" />

View File

@ -19,6 +19,7 @@ namespace MediaBrowser.Model.Notifications
NewLibraryContentMultiple,
ServerRestartRequired,
TaskFailed,
CameraImageUploaded
CameraImageUploaded,
UserLockedOut
}
}

View File

@ -59,6 +59,8 @@ namespace MediaBrowser.Model.Users
public string[] EnabledFolders { get; set; }
public bool EnableAllFolders { get; set; }
public int InvalidLoginAttemptCount { get; set; }
public UserPolicy()
{
EnableLiveTvManagement = true;

View File

@ -397,7 +397,10 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
hasLocalMetadata = true;
if (IsFullLocalMetadata(localItem.Item))
{
hasLocalMetadata = true;
}
successfulProviderCount++;
break;
}
@ -473,6 +476,11 @@ namespace MediaBrowser.Providers.Manager
return refreshResult;
}
protected virtual bool IsFullLocalMetadata(TItemType item)
{
return true;
}
private async Task ImportUserData(TItemType item, List<UserItemData> userDataList, CancellationToken cancellationToken)
{
var hasUserData = item as IHasUserData;

View File

@ -33,5 +33,22 @@ namespace MediaBrowser.Providers.Movies
target.TmdbCollectionName = source.TmdbCollectionName;
}
}
protected override bool IsFullLocalMetadata(Movie item)
{
if (string.IsNullOrWhiteSpace(item.Name))
{
return false;
}
if (string.IsNullOrWhiteSpace(item.Overview))
{
return false;
}
if (!item.ProductionYear.HasValue)
{
return false;
}
return base.IsFullLocalMetadata(item);
}
}
}

View File

@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Photos
try
{
var size = _imageProcessor.GetImageSize(imageInfo.Path, imageInfo.DateModified);
var size = _imageProcessor.GetImageSize(imageInfo);
item.Width = Convert.ToInt32(size.Width);
item.Height = Convert.ToInt32(size.Height);

View File

@ -74,5 +74,22 @@ namespace MediaBrowser.Providers.TV
await provider.Run(item, CancellationToken.None).ConfigureAwait(false);
}
}
protected override bool IsFullLocalMetadata(Series item)
{
if (string.IsNullOrWhiteSpace(item.Name))
{
return false;
}
if (string.IsNullOrWhiteSpace(item.Overview))
{
return false;
}
if (!item.ProductionYear.HasValue)
{
return false;
}
return base.IsFullLocalMetadata(item);
}
}
}

View File

@ -1,5 +1,6 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using System;
using System.IO;
using System.Linq;
@ -14,6 +15,11 @@ namespace MediaBrowser.Server.Implementations.Devices
public override bool IsVisible(User user)
{
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
return false;
}
return GetChildren(user, true).Any() &&
base.IsVisible(user);
}

View File

@ -62,8 +62,9 @@ namespace MediaBrowser.Server.Implementations.Drawing
logger.Info("Failed to read image header for {0}. Doing it the slow way.", path);
}
using (var wand = new MagickWand(path))
using (var wand = new MagickWand())
{
wand.PingImage(path);
var img = wand.CurrentImage;
return new ImageSize

View File

@ -350,9 +350,9 @@ namespace MediaBrowser.Server.Implementations.Drawing
}
/// <summary>
/// Increment this when indicator drawings change
/// Increment this when there's a change requiring caches to be invalidated
/// </summary>
private const string IndicatorVersion = "2";
private const string Version = "3";
/// <summary>
/// Gets the cache file path based on a set of parameters
@ -371,29 +371,19 @@ namespace MediaBrowser.Server.Implementations.Drawing
filename += "f=" + format;
var hasIndicator = false;
if (addPlayedIndicator)
{
filename += "pl=true";
hasIndicator = true;
}
if (percentPlayed > 0)
{
filename += "p=" + percentPlayed;
hasIndicator = true;
}
if (unwatchedCount.HasValue)
{
filename += "p=" + unwatchedCount.Value;
hasIndicator = true;
}
if (hasIndicator)
{
filename += "iv=" + IndicatorVersion;
}
if (!string.IsNullOrEmpty(backgroundColor))
@ -401,6 +391,8 @@ namespace MediaBrowser.Server.Implementations.Drawing
filename += "b=" + backgroundColor;
}
filename += "v=" + Version;
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLower());
}
@ -414,6 +406,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
return GetImageSize(path, File.GetLastWriteTimeUtc(path));
}
public ImageSize GetImageSize(ItemImageInfo info)
{
return GetImageSize(info.Path, info.DateModified);
}
/// <summary>
/// Gets the size of the image.
/// </summary>
@ -421,7 +418,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
/// <param name="imageDateModified">The image date modified.</param>
/// <returns>ImageSize.</returns>
/// <exception cref="System.ArgumentNullException">path</exception>
public ImageSize GetImageSize(string path, DateTime imageDateModified)
private ImageSize GetImageSize(string path, DateTime imageDateModified)
{
if (string.IsNullOrEmpty(path))
{
@ -666,30 +663,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
return enhancedImagePath;
}
private ImageFormat GetFormat(string path)
{
var extension = Path.GetExtension(path);
if (string.Equals(extension, ".png", StringComparison.OrdinalIgnoreCase))
{
return ImageFormat.Png;
}
if (string.Equals(extension, ".gif", StringComparison.OrdinalIgnoreCase))
{
return ImageFormat.Gif;
}
if (string.Equals(extension, ".webp", StringComparison.OrdinalIgnoreCase))
{
return ImageFormat.Webp;
}
if (string.Equals(extension, ".bmp", StringComparison.OrdinalIgnoreCase))
{
return ImageFormat.Bmp;
}
return ImageFormat.Jpg;
}
/// <summary>
/// Executes the image enhancers.
/// </summary>

View File

@ -1598,14 +1598,11 @@ namespace MediaBrowser.Server.Implementations.Dto
var path = imageInfo.Path;
// See if we can avoid a file system lookup by looking for the file in ResolveArgs
var dateModified = imageInfo.DateModified;
ImageSize size;
try
{
size = _imageProcessor.GetImageSize(path, dateModified);
size = _imageProcessor.GetImageSize(imageInfo);
}
catch (FileNotFoundException)
{

View File

@ -86,6 +86,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
_userManager.UserPasswordChanged += _userManager_UserPasswordChanged;
_userManager.UserDeleted += _userManager_UserDeleted;
_userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated;
_userManager.UserLockedOut += _userManager_UserLockedOut;
//_config.ConfigurationUpdated += _config_ConfigurationUpdated;
//_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
@ -95,6 +96,16 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
_appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
}
void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e)
{
CreateLogEntry(new ActivityLogEntry
{
Name = string.Format(_localization.GetLocalizedString("UserLockedOutWithName"), e.Argument.Name),
Type = "UserLockedOut",
UserId = e.Argument.Id.ToString("N")
});
}
void _subManager_SubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
CreateLogEntry(new ActivityLogEntry
@ -482,6 +493,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
_userManager.UserPasswordChanged -= _userManager_UserPasswordChanged;
_userManager.UserDeleted -= _userManager_UserDeleted;
_userManager.UserConfigurationUpdated -= _userManager_UserConfigurationUpdated;
_userManager.UserLockedOut -= _userManager_UserLockedOut;
_config.ConfigurationUpdated -= _config_ConfigurationUpdated;
_config.NamedConfigurationUpdated -= _config_NamedConfigurationUpdated;

View File

@ -78,6 +78,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
_appHost.HasUpdateAvailableChanged += _appHost_HasUpdateAvailableChanged;
_appHost.ApplicationUpdated += _appHost_ApplicationUpdated;
_deviceManager.CameraImageUploaded +=_deviceManager_CameraImageUploaded;
_userManager.UserLockedOut += _userManager_UserLockedOut;
}
async void _userManager_UserLockedOut(object sender, GenericEventArgs<User> e)
{
var type = NotificationType.UserLockedOut.ToString();
var notification = new NotificationRequest
{
NotificationType = type
};
notification.Variables["UserName"] = e.Argument.Name;
await SendNotification(notification).ConfigureAwait(false);
}
async void _deviceManager_CameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
@ -235,7 +251,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
return;
}
var notification = new NotificationRequest
{
NotificationType = type
@ -471,6 +486,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
_appHost.ApplicationUpdated -= _appHost_ApplicationUpdated;
_deviceManager.CameraImageUploaded -= _deviceManager_CameraImageUploaded;
_userManager.UserLockedOut -= _userManager_UserLockedOut;
}
private void DisposeLibraryUpdateTimer()

View File

@ -461,10 +461,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
{
return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest)
{
Throttle = options.Throttle,
ThrottleLimit = options.ThrottleLimit,
MinThrottlePosition = options.MinThrottlePosition,
ThrottleCallback = options.ThrottleCallback,
OnComplete = options.OnComplete
};
}
@ -480,10 +476,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return new StreamWriter(stream, contentType, _logger)
{
Throttle = options.Throttle,
ThrottleLimit = options.ThrottleLimit,
MinThrottlePosition = options.MinThrottlePosition,
ThrottleCallback = options.ThrottleCallback,
OnComplete = options.OnComplete
};
}

View File

@ -24,10 +24,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
private long RangeLength { get; set; }
private long TotalContentLength { get; set; }
public bool Throttle { get; set; }
public long ThrottleLimit { get; set; }
public long MinThrottlePosition;
public Func<long, long, long> ThrottleCallback { get; set; }
public Action OnComplete { get; set; }
/// <summary>
@ -165,14 +161,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="responseStream">The response stream.</param>
public void WriteTo(Stream responseStream)
{
if (Throttle)
{
responseStream = new ThrottledStream(responseStream, ThrottleLimit)
{
MinThrottlePosition = MinThrottlePosition,
ThrottleCallback = ThrottleCallback
};
}
WriteToInternal(responseStream);
}

View File

@ -35,10 +35,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
get { return _options; }
}
public bool Throttle { get; set; }
public long ThrottleLimit { get; set; }
public long MinThrottlePosition;
public Func<long, long, long> ThrottleCallback { get; set; }
public Action OnComplete { get; set; }
/// <summary>
@ -82,14 +78,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// <param name="responseStream">The response stream.</param>
public void WriteTo(Stream responseStream)
{
if (Throttle)
{
responseStream = new ThrottledStream(responseStream, ThrottleLimit)
{
MinThrottlePosition = MinThrottlePosition,
ThrottleCallback = ThrottleCallback
};
}
WriteToInternal(responseStream);
}

View File

@ -1,6 +1,7 @@
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
@ -47,5 +48,59 @@ namespace MediaBrowser.Server.Implementations.Library
{
return true;
}
public IEnumerable<MediaStream> GetMediaStreams(string mediaSourceId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
ItemId = new Guid(mediaSourceId)
});
return GetMediaStreamsForItem(list);
}
public IEnumerable<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
ItemId = itemId
});
return GetMediaStreamsForItem(list);
}
private IEnumerable<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams)
{
var list = streams.ToList();
var subtitleStreams = list
.Where(i => i.Type == MediaStreamType.Subtitle)
.ToList();
if (subtitleStreams.Count > 0)
{
var videoStream = list.FirstOrDefault(i => i.Type == MediaStreamType.Video);
// This is abitrary but at some point it becomes too slow to extract subtitles on the fly
// We need to learn more about when this is the case vs. when it isn't
const int maxAllowedBitrateForExternalSubtitleStream = 10000000;
var videoBitrate = videoStream == null ? maxAllowedBitrateForExternalSubtitleStream : videoStream.BitRate ?? maxAllowedBitrateForExternalSubtitleStream;
foreach (var subStream in subtitleStreams)
{
var supportsExternalStream = StreamSupportsExternalStream(subStream);
if (supportsExternalStream && videoBitrate >= maxAllowedBitrateForExternalSubtitleStream)
{
supportsExternalStream = false;
}
subStream.SupportsExternalStream = supportsExternalStream;
}
}
return list;
}
}
}

View File

@ -1,19 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using System;
using System.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Naming.Common;
using MediaBrowser.Naming.IO;
using MediaBrowser.Naming.TV;
using MediaBrowser.Server.Implementations.Logging;
using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
{

View File

@ -97,6 +97,7 @@ namespace MediaBrowser.Server.Implementations.Library
/// </summary>
public event EventHandler<GenericEventArgs<User>> UserUpdated;
public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
public event EventHandler<GenericEventArgs<User>> UserLockedOut;
/// <summary>
/// Called when [user updated].
@ -259,6 +260,11 @@ namespace MediaBrowser.Server.Implementations.Library
{
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
await UpdateUser(user).ConfigureAwait(false);
await UpdateInvalidLoginAttemptCount(user, 0).ConfigureAwait(false);
}
else
{
await UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1).ConfigureAwait(false);
}
_logger.Info("Authentication request for {0} {1}.", user.Name, (success ? "has succeeded" : "has been denied"));
@ -266,6 +272,38 @@ namespace MediaBrowser.Server.Implementations.Library
return success;
}
private async Task UpdateInvalidLoginAttemptCount(User user, int newValue)
{
if (user.Policy.InvalidLoginAttemptCount != newValue || newValue > 0)
{
user.Policy.InvalidLoginAttemptCount = newValue;
var maxCount = user.Policy.IsAdministrator ?
3 :
5;
var fireLockout = false;
if (newValue >= maxCount)
{
_logger.Debug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue.ToString(CultureInfo.InvariantCulture));
user.Policy.IsDisabled = true;
fireLockout = true;
}
await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false);
if (fireLockout)
{
if (UserLockedOut != null)
{
EventHelper.FireEventIfNotNull(UserLockedOut, this, new GenericEventArgs<User>(user), _logger);
}
}
}
}
private string GetPasswordHash(User user)
{
return string.IsNullOrEmpty(user.Password)
@ -332,11 +370,6 @@ namespace MediaBrowser.Server.Implementations.Library
{
if (!user.Configuration.HasMigratedToPolicy)
{
user.Policy.BlockUnratedItems = user.Configuration.BlockUnratedItems;
user.Policy.EnableContentDeletion = user.Configuration.EnableContentDeletion;
user.Policy.EnableLiveTvAccess = user.Configuration.EnableLiveTvAccess;
user.Policy.EnableLiveTvManagement = user.Configuration.EnableLiveTvManagement;
user.Policy.EnableMediaPlayback = user.Configuration.EnableMediaPlayback;
user.Policy.IsAdministrator = user.Configuration.IsAdministrator;
await UpdateUserPolicy(user, user.Policy, false);
@ -915,10 +948,6 @@ namespace MediaBrowser.Server.Implementations.Library
}
user.Configuration.IsAdministrator = user.Policy.IsAdministrator;
user.Configuration.EnableLiveTvManagement = user.Policy.EnableLiveTvManagement;
user.Configuration.EnableLiveTvAccess = user.Policy.EnableLiveTvAccess;
user.Configuration.EnableMediaPlayback = user.Policy.EnableMediaPlayback;
user.Configuration.EnableContentDeletion = user.Policy.EnableContentDeletion;
await UpdateConfiguration(user, user.Configuration, true).ConfigureAwait(false);
}

View File

@ -442,7 +442,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return null;
}
private const string InternalVersionNumber = "3";
private const string InternalVersionNumber = "4";
public Guid GetInternalChannelId(string serviceName, string externalId)
{

View File

@ -48,8 +48,10 @@
"LabelDashboardSourcePathHelp": "If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location.",
"ButtonConvertMedia": "Convert media",
"ButtonOrganize": "Organize",
"LabelPinCode": "Pin code:",
"ButtonOk": "Ok",
"ButtonCancel": "Cancel",
"ButtonExit": "Exit",
"ButtonNew": "New",
"HeaderTV": "TV",
"HeaderAudio": "Audio",
@ -57,6 +59,12 @@
"HeaderPaths": "Paths",
"CategorySync": "Sync",
"HeaderEasyPinCode": "Easy Pin Code",
"HeaderGrownupsOnly": "Grown-ups Only!",
"DividerOr": "-- or --",
"HeaderToAccessPleaseEnterEasyPinCode": "To access, please enter your easy pin code",
"KidsModeAdultInstruction": "Click the lock icon in the bottom right to configure or leave kids mode. Your pin code will be required.",
"ButtonConfigurePinCode": "Configure pin code",
"HeaderAdultsReadHere": "Adults Read Here!",
"RegisterWithPayPal": "Register with PayPal",
"HeaderSyncRequiresSupporterMembership": "Sync Requires a Supporter Membership",
"HeaderEnjoyDayTrial": "Enjoy a 14 Day Free Trial",
@ -670,6 +678,7 @@
"NotificationOptionNewLibraryContent": "New content added",
"NotificationOptionNewLibraryContentMultiple": "New content added (multiple)",
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
"NotificationOptionUserLockedOut": "User locked out",
"SendNotificationHelp": "By default, notifications are delivered to the dashboard inbox. Browse the plugin catalog to install additional notification options.",
"NotificationOptionServerRestartRequired": "Server restart required",
"LabelNotificationEnabled": "Enable this notification",
@ -1061,6 +1070,7 @@
"OptionBox": "Box",
"OptionBoxRear": "Box rear",
"OptionDisc": "Disc",
"OptionIcon": "Icon",
"OptionLogo": "Logo",
"OptionMenu": "Menu",
"OptionScreenshot": "Screenshot",
@ -1105,6 +1115,7 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"LabelRunningTimeValue": "Running time: {0}",
"LabelIpAddressValue": "Ip address: {0}",
"UserLockedOutWithName": "User {0} has been locked out",
"UserConfigurationUpdatedWithName": "User configuration has been updated for {0}",
"UserCreatedWithName": "User {0} has been created",
"UserPasswordChangedWithName": "Password has been changed for user {0}",
@ -1114,7 +1125,7 @@
"MessageApplicationUpdated": "Media Browser Server has been updated",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
"UserStartedPlayingItemWithValues": "{0} has started playing {1}",
"UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}",
"AppDeviceValues": "App: {0}, Device: {1}",
@ -1369,5 +1380,7 @@
"TabJobs": "Jobs",
"TabSyncJobs": "Sync Jobs",
"LabelTagFilterMode": "Mode:",
"LabelTagFilterAllowModeHelp": "If allowed tags are used as part of a deeply nested folder structure, content that is tagged will require parent folders to be tagged as well."
"LabelTagFilterAllowModeHelp": "If allowed tags are used as part of a deeply nested folder structure, content that is tagged will require parent folders to be tagged as well.",
"HeaderThisUserIsCurrentlyDisabled": "This user is currently disabled",
"MessageReenableUser": "See below to reenable"
}

View File

@ -47,7 +47,7 @@
<ItemGroup>
<Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\ImageMagickSharp.1.0.0.2\lib\net45\ImageMagickSharp.dll</HintPath>
<HintPath>..\packages\ImageMagickSharp.1.0.0.6\lib\net45\ImageMagickSharp.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.Naming, Version=1.0.5509.27636, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
@ -278,6 +278,7 @@
<Compile Include="Sorting\CommunityRatingComparer.cs" />
<Compile Include="Sorting\CriticRatingComparer.cs" />
<Compile Include="Sorting\DateCreatedComparer.cs" />
<Compile Include="Sorting\DateLastMediaAddedComparer.cs" />
<Compile Include="Sorting\DatePlayedComparer.cs" />
<Compile Include="Sorting\GameSystemComparer.cs" />
<Compile Include="Sorting\IsFavoriteOrLikeComparer.cs" />
@ -303,8 +304,12 @@
<Compile Include="Sorting\StudioComparer.cs" />
<Compile Include="Sorting\VideoBitRateComparer.cs" />
<Compile Include="Sync\AppSyncProvider.cs" />
<Compile Include="Sync\CloudSyncProvider.cs" />
<Compile Include="Sync\FolderSync\FolderSyncDataProvider.cs" />
<Compile Include="Sync\FolderSync\FolderSyncProvider.cs" />
<Compile Include="Sync\CloudSyncProfile.cs" />
<Compile Include="Sync\IHasSyncProfile.cs" />
<Compile Include="Sync\MediaSync.cs" />
<Compile Include="Sync\MultiProviderSync.cs" />
<Compile Include="Sync\SyncRegistrationInfo.cs" />
<Compile Include="Sync\SyncConfig.cs" />
<Compile Include="Sync\SyncJobProcessor.cs" />

View File

@ -143,6 +143,13 @@ namespace MediaBrowser.Server.Implementations.Notifications
Type = NotificationType.CameraImageUploaded.ToString(),
DefaultTitle = "A new camera image has been uploaded from {DeviceName}.",
Variables = new List<string>{"DeviceName"}
},
new NotificationTypeInfo
{
Type = NotificationType.UserLockedOut.ToString(),
DefaultTitle = "{UserName} has been locked out.",
Variables = new List<string>{"UserName"}
}
};
@ -185,6 +192,10 @@ namespace MediaBrowser.Server.Implementations.Notifications
{
note.Category = _localization.GetLocalizedString("CategorySync");
}
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
{
note.Category = _localization.GetLocalizedString("CategoryUser");
}
else
{
note.Category = _localization.GetLocalizedString("CategorySystem");

View File

@ -108,7 +108,12 @@ namespace MediaBrowser.Server.Implementations.Photos
protected Task<Stream> GetThumbCollage(List<BaseItem> items)
{
return DynamicImageHelpers.GetThumbCollage(items.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)).ToList(),
var files = items
.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb))
.Where(i => !string.IsNullOrWhiteSpace(i))
.ToList();
return DynamicImageHelpers.GetThumbCollage(files,
FileSystem,
1600,
900,
@ -117,7 +122,12 @@ namespace MediaBrowser.Server.Implementations.Photos
protected Task<Stream> GetSquareCollage(List<BaseItem> items)
{
return DynamicImageHelpers.GetSquareCollage(items.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb)).ToList(),
var files = items
.Select(i => i.GetImagePath(ImageType.Primary) ?? i.GetImagePath(ImageType.Thumb))
.Where(i => !string.IsNullOrWhiteSpace(i))
.ToList();
return DynamicImageHelpers.GetSquareCollage(files,
FileSystem,
800, ApplicationPaths);
}

View File

@ -4,6 +4,7 @@ using MediaBrowser.Common.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Photos
@ -15,6 +16,11 @@ namespace MediaBrowser.Server.Implementations.Photos
int width,
int height, IApplicationPaths appPaths)
{
if (files.Any(string.IsNullOrWhiteSpace))
{
throw new ArgumentException("Empty file found in files list");
}
if (files.Count < 3)
{
return await GetSingleImage(files, fileSystem).ConfigureAwait(false);
@ -27,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Photos
int cellHeight = height;
var index = 0;
using (var wand = new MagickWand(width, height, "transparent"))
using (var wand = new MagickWand(width, height, new PixelWand(ColorName.None, 1)))
{
for (var row = 0; row < rows; row++)
{
@ -57,6 +63,11 @@ namespace MediaBrowser.Server.Implementations.Photos
IFileSystem fileSystem,
int size, IApplicationPaths appPaths)
{
if (files.Any(string.IsNullOrWhiteSpace))
{
throw new ArgumentException("Empty file found in files list");
}
if (files.Count < 4)
{
return await GetSingleImage(files, fileSystem).ConfigureAwait(false);
@ -68,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Photos
int singleSize = size / 2;
var index = 0;
using (var wand = new MagickWand(size, size, "transparent"))
using (var wand = new MagickWand(size, size, new PixelWand(ColorName.None, 1)))
{
for (var row = 0; row < rows; row++)
{

View File

@ -399,7 +399,7 @@ namespace MediaBrowser.Server.Implementations.Session
Client = clientType,
DeviceId = deviceId,
ApplicationVersion = appVersion,
Id = Guid.NewGuid().ToString("N")
Id = key.GetMD5().ToString("N")
};
sessionInfo.DeviceName = deviceName;
@ -798,6 +798,19 @@ namespace MediaBrowser.Server.Implementations.Session
return session;
}
private SessionInfo GetSessionToRemoteControl(string sessionId)
{
// Accept either device id or session id
var session = Sessions.FirstOrDefault(i => string.Equals(i.Id, sessionId));
if (session == null)
{
throw new ResourceNotFoundException(string.Format("Session {0} not found.", sessionId));
}
return session;
}
public Task SendMessageCommand(string controllingSessionId, string sessionId, MessageCommand command, CancellationToken cancellationToken)
{
var generalCommand = new GeneralCommand
@ -818,7 +831,7 @@ namespace MediaBrowser.Server.Implementations.Session
public Task SendGeneralCommand(string controllingSessionId, string sessionId, GeneralCommand command, CancellationToken cancellationToken)
{
var session = GetSession(sessionId);
var session = GetSessionToRemoteControl(sessionId);
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
@ -828,7 +841,7 @@ namespace MediaBrowser.Server.Implementations.Session
public Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
{
var session = GetSession(sessionId);
var session = GetSessionToRemoteControl(sessionId);
var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null;
@ -955,7 +968,7 @@ namespace MediaBrowser.Server.Implementations.Session
public Task SendPlaystateCommand(string controllingSessionId, string sessionId, PlaystateRequest command, CancellationToken cancellationToken)
{
var session = GetSession(sessionId);
var session = GetSessionToRemoteControl(sessionId);
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
@ -1566,11 +1579,7 @@ namespace MediaBrowser.Server.Implementations.Session
if (!string.IsNullOrWhiteSpace(mediaSourceId))
{
info.MediaStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
{
ItemId = new Guid(mediaSourceId)
}).ToList();
info.MediaStreams = _mediaSourceManager.GetMediaStreams(mediaSourceId).ToList();
}
return info;

View File

@ -0,0 +1,70 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
using System;
using System.Linq;
namespace MediaBrowser.Server.Implementations.Sorting
{
public class DateLastMediaAddedComparer : IUserBaseItemComparer
{
/// <summary>
/// Gets or sets the user.
/// </summary>
/// <value>The user.</value>
public User User { get; set; }
/// <summary>
/// Gets or sets the user manager.
/// </summary>
/// <value>The user manager.</value>
public IUserManager UserManager { get; set; }
/// <summary>
/// Gets or sets the user data repository.
/// </summary>
/// <value>The user data repository.</value>
public IUserDataManager UserDataRepository { get; set; }
/// <summary>
/// Compares the specified x.
/// </summary>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <returns>System.Int32.</returns>
public int Compare(BaseItem x, BaseItem y)
{
return GetDate(x).CompareTo(GetDate(y));
}
/// <summary>
/// Gets the date.
/// </summary>
/// <param name="x">The x.</param>
/// <returns>DateTime.</returns>
private DateTime GetDate(BaseItem x)
{
var folder = x as Folder;
if (folder != null)
{
return folder.GetRecursiveChildren(User, i => !i.IsFolder)
.Select(i => i.DateCreated)
.OrderByDescending(i => i)
.FirstOrDefault();
}
return x.DateCreated;
}
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
public string Name
{
get { return ItemSortBy.DateLastContentAdded; }
}
}
}

View File

@ -1,6 +1,5 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
using System;

View File

@ -8,7 +8,7 @@ using System.Linq;
namespace MediaBrowser.Server.Implementations.Sync
{
public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds
public class AppSyncProvider : ISyncProvider, IHasUniqueTargetIds, IHasSyncProfile
{
private readonly IDeviceManager _deviceManager;
@ -42,5 +42,18 @@ namespace MediaBrowser.Server.Implementations.Sync
{
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
});
}
}
}

View File

@ -0,0 +1,118 @@
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
}
};
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
}
};
}
}
}

View File

@ -1,59 +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"; }
}
private ICloudSyncProvider GetProvider(SyncTarget target)
{
return null;
}
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 inputFile, string[] pathParts, SyncTarget target, CancellationToken cancellationToken)
{
var provider = GetProvider(target);
return provider.TransferItemFile(serverId, itemId, inputFile, pathParts, target, cancellationToken);
}
}
}

View File

@ -0,0 +1,31 @@
using MediaBrowser.Controller.Sync;
using MediaBrowser.Model.Sync;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Sync.FolderSync
{
public class FolderSyncDataProvider : ISyncDataProvider
{
public Task<List<string>> GetServerItemIds(SyncTarget target, string serverId)
{
throw new NotImplementedException();
}
public Task AddOrUpdate(SyncTarget target, LocalItem item)
{
throw new NotImplementedException();
}
public Task Delete(SyncTarget target, string id)
{
throw new NotImplementedException();
}
public Task<LocalItem> Get(SyncTarget target, string id)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,143 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sync;
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.FolderSync
{
public class FolderSyncProvider : IServerSyncProvider
{
private readonly IApplicationPaths _appPaths;
private readonly IUserManager _userManager;
public FolderSyncProvider(IApplicationPaths appPaths, IUserManager userManager)
{
_appPaths = appPaths;
_userManager = userManager;
}
public Task SendFile(string inputFile, string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken)
{
return Task.Run(() => File.Copy(inputFile, path, true), cancellationToken);
}
public Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken)
{
return Task.Run(() => File.Delete(path), cancellationToken);
}
public Task<Stream> GetFile(string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken)
{
return Task.FromResult((Stream)File.OpenRead(path));
}
public string GetFullPath(IEnumerable<string> paths, SyncTarget target)
{
var account = GetSyncAccounts()
.FirstOrDefault(i => string.Equals(i.Id, target.Id, StringComparison.OrdinalIgnoreCase));
if (account == null)
{
throw new ArgumentException("Invalid SyncTarget supplied.");
}
var list = paths.ToList();
list.Insert(0, account.Path);
return Path.Combine(list.ToArray());
}
public string GetParentDirectoryPath(string path, SyncTarget target)
{
return Path.GetDirectoryName(path);
}
public Task<List<DeviceFileInfo>> GetFileSystemEntries(string path, SyncTarget target)
{
List<FileInfo> files;
try
{
files = new DirectoryInfo(path).EnumerateFiles("*", SearchOption.TopDirectoryOnly).ToList();
}
catch (DirectoryNotFoundException)
{
files = new List<FileInfo>();
}
return Task.FromResult(files.Select(i => new DeviceFileInfo
{
Name = i.Name,
Path = i.FullName
}).ToList());
}
public ISyncDataProvider GetDataProvider()
{
// If single instances are needed, manage them here
return new FolderSyncDataProvider();
}
public string Name
{
get { return "Folder Sync"; }
}
public IEnumerable<SyncTarget> GetSyncTargets(string userId)
{
return GetSyncAccounts()
.Where(i => i.UserIds.Contains(userId, StringComparer.OrdinalIgnoreCase))
.Select(GetSyncTarget);
}
public IEnumerable<SyncTarget> GetAllSyncTargets()
{
return GetSyncAccounts().Select(GetSyncTarget);
}
private SyncTarget GetSyncTarget(SyncAccount account)
{
return new SyncTarget
{
Id = account.Id,
Name = account.Name
};
}
private IEnumerable<SyncAccount> GetSyncAccounts()
{
return new List<SyncAccount>();
// Dummy this up
return _userManager
.Users
.Select(i => new SyncAccount
{
Id = i.Id.ToString("N"),
UserIds = new List<string> { i.Id.ToString("N") },
Path = Path.Combine(_appPaths.DataPath, "foldersync", i.Id.ToString("N")),
Name = i.Name + "'s Folder Sync"
});
}
// An internal class to manage all configured Folder Sync accounts for differnet users
class SyncAccount
{
public string Id { get; set; }
public string Name { get; set; }
public string Path { get; set; }
public List<string> UserIds { get; set; }
public SyncAccount()
{
UserIds = new List<string>();
}
}
}
}

View File

@ -0,0 +1,15 @@
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Sync;
namespace MediaBrowser.Server.Implementations.Sync
{
public interface IHasSyncProfile
{
/// <summary>
/// Gets the device profile.
/// </summary>
/// <param name="target">The target.</param>
/// <returns>DeviceProfile.</returns>
DeviceProfile GetDeviceProfile(SyncTarget target);
}
}

View File

@ -1,10 +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;
@ -15,22 +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,
ISyncDataProvider dataProvider,
SyncTarget target,
IProgress<double> progress,
CancellationToken cancellationToken)
{
var serverId = _appHost.SystemId;
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>();
@ -40,20 +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, serverId, target, cancellationToken).ConfigureAwait(false);
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
{
@ -68,22 +80,23 @@ 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;
@ -106,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;
@ -117,6 +130,7 @@ namespace MediaBrowser.Server.Implementations.Sync
}
private async Task GetItem(IServerSyncProvider provider,
ISyncDataProvider dataProvider,
SyncTarget target,
string serverId,
SyncedItem jobItem,
@ -129,6 +143,8 @@ namespace MediaBrowser.Server.Implementations.Sync
var fileTransferProgress = new ActionableProgress<double>();
fileTransferProgress.RegisterAction(pct => progress.Report(pct * .92));
var localItem = CreateLocalItem(provider, target, libraryItem, serverId, jobItem.OriginalFileName);
await _syncManager.ReportSyncJobItemTransferBeginning(internalSyncJobItem.Id);
var transferSuccess = false;
@ -136,10 +152,10 @@ namespace MediaBrowser.Server.Implementations.Sync
try
{
string[] pathParts = GetPathParts(serverId, libraryItem);
await SendFile(provider, internalSyncJobItem.OutputPath, localItem, target, cancellationToken).ConfigureAwait(false);
await provider.TransferItemFile(serverId, libraryItem.Id, internalSyncJobItem.OutputPath, pathParts, target, cancellationToken)
.ConfigureAwait(false);
// Create db record
await dataProvider.AddOrUpdate(target, localItem).ConfigureAwait(false);
progress.Report(92);
@ -165,18 +181,189 @@ 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 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 string[] GetPathParts(string serverId, BaseItemDto item)
private Task SendFile(IServerSyncProvider provider, string inputPath, LocalItem item, SyncTarget target, CancellationToken cancellationToken)
{
return null;
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, SyncTarget target, BaseItemDto libraryItem, string serverId, string originalFileName)
{
var path = GetDirectoryPath(provider, 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, BaseItemDto item, string serverId)
{
var parts = new List<string>
{
serverId
};
if (item.IsType("episode"))
{
parts.Add("TV");
parts.Add(item.SeriesName);
if (!string.IsNullOrWhiteSpace(item.SeasonName))
{
parts.Add(item.SeasonName);
}
}
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.IsNullOrEmpty(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);
}
}
}

View File

@ -0,0 +1,69 @@
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 ISyncManager _syncManager;
private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
public MultiProviderSync(ISyncManager 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);
});
await new MediaSync(_logger, _syncManager, _appHost, _fileSystem)
.Sync(target.Item1, target.Item1.GetDataProvider(), target.Item2, innerProgress, cancellationToken)
.ConfigureAwait(false);
numComplete++;
startingPercent = numComplete;
startingPercent /= targets.Count;
startingPercent *= 100;
progress.Report(startingPercent);
}
}
}
}

View File

@ -407,6 +407,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
@ -429,13 +438,6 @@ namespace MediaBrowser.Server.Implementations.Sync
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)));
}
private string GetSyncProviderId(ISyncProvider provider)
{
return (provider.GetType().Name).GetMD5().ToString("N");
@ -543,11 +545,11 @@ namespace MediaBrowser.Server.Implementations.Sync
{
foreach (var provider in _providers)
{
foreach (var target in GetSyncTargets(provider, null))
foreach (var target in GetSyncTargets(provider))
{
if (string.Equals(target.Id, targetId, StringComparison.OrdinalIgnoreCase))
{
return provider.GetDeviceProfile(target);
return GetDeviceProfile(provider, target);
}
}
}
@ -555,6 +557,18 @@ namespace MediaBrowser.Server.Implementations.Sync
return null;
}
public DeviceProfile GetDeviceProfile(ISyncProvider provider, SyncTarget target)
{
var hasProfile = provider as IHasSyncProfile;
if (hasProfile != null)
{
return hasProfile.GetDeviceProfile(target);
}
return new CloudSyncProfile(true, false);
}
public async Task ReportSyncJobItemTransferred(string id)
{
var jobItem = _repo.GetJobItem(id);

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ImageMagickSharp" version="1.0.0.2" targetFramework="net45" />
<package id="ImageMagickSharp" version="1.0.0.6" targetFramework="net45" />
<package id="MediaBrowser.Naming" version="1.0.0.32" targetFramework="net45" />
<package id="Mono.Nat" version="1.2.21.0" targetFramework="net45" />
<package id="morelinq" version="1.1.0" targetFramework="net45" />

View File

@ -0,0 +1,25 @@
using MediaBrowser.Controller.Diagnostics;
using System.Diagnostics;
namespace MediaBrowser.Server.Mono.Diagnostics
{
public class LinuxProcessManager : IProcessManager
{
public bool SupportsSuspension
{
get { return true; }
}
public void SuspendProcess(Process process)
{
// http://jumptuck.com/2011/11/23/quick-tip-pause-process-linux/
process.StandardInput.WriteLine("^Z");
}
public void ResumeProcess(Process process)
{
// http://jumptuck.com/2011/11/23/quick-tip-pause-process-linux/
process.StandardInput.WriteLine("fg");
}
}
}

View File

@ -74,6 +74,7 @@
<Compile Include="..\SharedVersion.cs">
<Link>Properties\SharedVersion.cs</Link>
</Compile>
<Compile Include="Diagnostics\LinuxProcessManager.cs" />
<Compile Include="Native\BaseMonoApp.cs" />
<Compile Include="Networking\CertificateGenerator.cs" />
<Compile Include="Program.cs" />

View File

@ -1,6 +1,8 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.IsoMounter;
using MediaBrowser.Model.Logging;
using MediaBrowser.Server.Mono.Diagnostics;
using MediaBrowser.Server.Mono.Networking;
using MediaBrowser.Server.Startup.Common;
using Mono.Unix.Native;
@ -189,5 +191,16 @@ namespace MediaBrowser.Server.Mono.Native
public string sysname = string.Empty;
public string machine = string.Empty;
}
public IProcessManager GetProcessManager()
{
if (Environment.OperatingSystem == Startup.Common.OperatingSystem.Linux)
{
return new LinuxProcessManager();
}
return new ProcessManager();
}
}
}

View File

@ -380,6 +380,8 @@ namespace MediaBrowser.Server.Startup.Common
RegisterSingleInstance(ServerConfigurationManager);
RegisterSingleInstance(NativeApp.GetProcessManager());
LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer);
RegisterSingleInstance(LocalizationManager);

View File

@ -0,0 +1,23 @@
using MediaBrowser.Controller.Diagnostics;
using System.Diagnostics;
namespace MediaBrowser.Server.Mono.Diagnostics
{
public class ProcessManager : IProcessManager
{
public void SuspendProcess(Process process)
{
process.PriorityClass = ProcessPriorityClass.Idle;
}
public void ResumeProcess(Process process)
{
process.PriorityClass = ProcessPriorityClass.Normal;
}
public bool SupportsSuspension
{
get { return true; }
}
}
}

View File

@ -1,4 +1,5 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.Model.Logging;
using System.Collections.Generic;
using System.Reflection;
@ -84,5 +85,11 @@ namespace MediaBrowser.Server.Startup.Common
/// Prevents the system stand by.
/// </summary>
void PreventSystemStandby();
/// <summary>
/// Gets the process manager.
/// </summary>
/// <returns>IProcessManager.</returns>
IProcessManager GetProcessManager();
}
}

View File

@ -56,6 +56,7 @@
<Compile Include="ApplicationHost.cs" />
<Compile Include="ApplicationPathHelper.cs" />
<Compile Include="Browser\BrowserLauncher.cs" />
<Compile Include="Diagnostics\ProcessManager.cs" />
<Compile Include="EntryPoints\KeepServerAwake.cs" />
<Compile Include="EntryPoints\StartupWizard.cs" />
<Compile Include="FFMpeg\FFMpegDownloader.cs" />

View File

@ -62,7 +62,7 @@
<ItemGroup>
<Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\ImageMagickSharp.1.0.0.2\lib\net45\ImageMagickSharp.dll</HintPath>
<HintPath>..\packages\ImageMagickSharp.1.0.0.6\lib\net45\ImageMagickSharp.dll</HintPath>
</Reference>
<Reference Include="MediaBrowser.IsoMounter">
<HintPath>..\packages\MediaBrowser.IsoMounting.3.0.69\lib\net45\MediaBrowser.IsoMounter.dll</HintPath>
@ -113,6 +113,7 @@
<Compile Include="Native\Standby.cs" />
<Compile Include="Native\ServerAuthorization.cs" />
<Compile Include="Native\WindowsApp.cs" />
<Compile Include="Native\WindowsProcessManager.cs" />
<Compile Include="Networking\CertificateGenerator.cs" />
<Compile Include="Networking\NativeMethods.cs" />
<Compile Include="Networking\NetworkManager.cs" />

View File

@ -1,7 +1,9 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Diagnostics;
using MediaBrowser.IsoMounter;
using MediaBrowser.Model.Logging;
using MediaBrowser.Server.Mono.Diagnostics;
using MediaBrowser.Server.Startup.Common;
using MediaBrowser.ServerApplication.Networking;
using System.Collections.Generic;
@ -109,5 +111,10 @@ namespace MediaBrowser.ServerApplication.Native
{
Standby.PreventSystemStandby();
}
public IProcessManager GetProcessManager()
{
return new WindowsProcessManager();
}
}
}

View File

@ -0,0 +1,78 @@
using MediaBrowser.Controller.Diagnostics;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace MediaBrowser.ServerApplication.Native
{
public class WindowsProcessManager : IProcessManager
{
public void SuspendProcess(Process process)
{
process.Suspend();
}
public void ResumeProcess(Process process)
{
process.Resume();
}
public bool SupportsSuspension
{
get { return true; }
}
}
public static class ProcessExtension
{
[DllImport("kernel32.dll")]
static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
[DllImport("kernel32.dll")]
static extern uint SuspendThread(IntPtr hThread);
[DllImport("kernel32.dll")]
static extern int ResumeThread(IntPtr hThread);
public static void Suspend(this Process process)
{
foreach (ProcessThread thread in process.Threads)
{
var pOpenThread = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id);
if (pOpenThread == IntPtr.Zero)
{
break;
}
SuspendThread(pOpenThread);
}
}
public static void Resume(this Process process)
{
foreach (ProcessThread thread in process.Threads)
{
var pOpenThread = OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint)thread.Id);
if (pOpenThread == IntPtr.Zero)
{
break;
}
ResumeThread(pOpenThread);
}
}
public static void Print(this Process process)
{
Console.WriteLine("{0,8} {1}", process.Id, process.ProcessName);
}
}
[Flags]
public enum ThreadAccess : int
{
TERMINATE = (0x0001),
SUSPEND_RESUME = (0x0002),
GET_CONTEXT = (0x0008),
SET_CONTEXT = (0x0010),
SET_INFORMATION = (0x0020),
QUERY_INFORMATION = (0x0040),
SET_THREAD_TOKEN = (0x0080),
IMPERSONATE = (0x0100),
DIRECT_IMPERSONATION = (0x0200)
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ImageMagickSharp" version="1.0.0.2" targetFramework="net45" />
<package id="ImageMagickSharp" version="1.0.0.6" targetFramework="net45" />
<package id="MediaBrowser.IsoMounting" version="3.0.69" targetFramework="net45" />
<package id="System.Data.SQLite.Core" version="1.0.94.0" targetFramework="net45" />
</packages>

View File

@ -421,6 +421,7 @@ namespace MediaBrowser.WebDashboard.Api
"itembynamedetailpage.js",
"itemdetailpage.js",
"itemlistpage.js",
"kids.js",
"librarypathmapping.js",
"reports.js",
"librarysettings.js",

View File

@ -87,6 +87,9 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="dashboard-ui\css\images\kids\bg.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\css\images\server.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -114,6 +117,9 @@
<Content Include="dashboard-ui\forgotpasswordpin.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\kids.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\mysync.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -129,6 +135,9 @@
<Content Include="dashboard-ui\scripts\forgotpasswordpin.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\kids.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\scripts\selectserver.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@ -214,7 +214,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
}
using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
using (var filestream = FileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
stream.CopyTo(filestream);
}

View File

@ -520,4 +520,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(Performance) = preSolution
HasPerformanceSessions = true
EndGlobalSection
EndGlobal

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Common.Internal</id>
<version>3.0.576</version>
<version>3.0.579</version>
<title>MediaBrowser.Common.Internal</title>
<authors>Luke</authors>
<owners>ebr,Luke,scottisafool</owners>
@ -12,7 +12,7 @@
<description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
<copyright>Copyright © Media Browser 2013</copyright>
<dependencies>
<dependency id="MediaBrowser.Common" version="3.0.576" />
<dependency id="MediaBrowser.Common" version="3.0.579" />
<dependency id="NLog" version="3.2.0.0" />
<dependency id="SimpleInjector" version="2.7.0" />
</dependencies>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Common</id>
<version>3.0.576</version>
<version>3.0.579</version>
<title>MediaBrowser.Common</title>
<authors>Media Browser Team</authors>
<owners>ebr,Luke,scottisafool</owners>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>MediaBrowser.Model.Signed</id>
<version>3.0.576</version>
<version>3.0.579</version>
<title>MediaBrowser.Model - Signed Edition</title>
<authors>Media Browser Team</authors>
<owners>ebr,Luke,scottisafool</owners>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>MediaBrowser.Server.Core</id>
<version>3.0.576</version>
<version>3.0.579</version>
<title>Media Browser.Server.Core</title>
<authors>Media Browser Team</authors>
<owners>ebr,Luke,scottisafool</owners>
@ -12,7 +12,7 @@
<description>Contains core components required to build plugins for Media Browser Server.</description>
<copyright>Copyright © Media Browser 2013</copyright>
<dependencies>
<dependency id="MediaBrowser.Common" version="3.0.576" />
<dependency id="MediaBrowser.Common" version="3.0.579" />
</dependencies>
</metadata>
<files>