Merge pull request #1991 from MediaBrowser/beta

Beta
This commit is contained in:
Luke 2016-07-27 15:32:07 -04:00 committed by GitHub
commit 06b0cfb86f
55 changed files with 2462 additions and 407 deletions

View File

@ -1669,7 +1669,7 @@ namespace MediaBrowser.Api.Playback
RequestedUrl = url, RequestedUrl = url,
UserAgent = Request.UserAgent UserAgent = Request.UserAgent
}; };
//if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || //if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 ||
// (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 || // (Request.UserAgent ?? string.Empty).IndexOf("ipad", StringComparison.OrdinalIgnoreCase) != -1 ||
// (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1) // (Request.UserAgent ?? string.Empty).IndexOf("ipod", StringComparison.OrdinalIgnoreCase) != -1)
@ -1770,6 +1770,19 @@ namespace MediaBrowser.Api.Playback
{ {
state.OutputVideoCodec = "copy"; state.OutputVideoCodec = "copy";
} }
else
{
// If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
if (!string.IsNullOrWhiteSpace(auth.UserId))
{
var user = UserManager.GetUserById(auth.UserId);
if (!user.Policy.EnableVideoPlaybackTranscoding)
{
state.OutputVideoCodec = "copy";
}
}
}
if (state.AudioStream != null && CanStreamCopyAudio(state, state.SupportedAudioCodecs)) if (state.AudioStream != null && CanStreamCopyAudio(state, state.SupportedAudioCodecs))
{ {

View File

@ -257,8 +257,7 @@ namespace MediaBrowser.Api.Playback.Hls
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
} }
// 256k private const int BufferSize = 81920;
private const int BufferSize = 262144;
private long GetStartPositionTicks(StreamState state, int requestedIndex) private long GetStartPositionTicks(StreamState state, int requestedIndex)
{ {
@ -942,17 +941,5 @@ namespace MediaBrowser.Api.Playback.Hls
{ {
return isOutputVideo ? ".ts" : ".ts"; return isOutputVideo ? ".ts" : ".ts";
} }
protected override bool CanStreamCopyVideo(StreamState state)
{
var isLiveStream = IsLiveStream(state);
//if (!isLiveStream && Request.QueryString["AllowCustomSegmenting"] != "true")
//{
// return false;
//}
return base.CanStreamCopyVideo(state);
}
} }
} }

View File

@ -15,6 +15,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
namespace MediaBrowser.Api.Playback namespace MediaBrowser.Api.Playback
@ -68,8 +69,9 @@ namespace MediaBrowser.Api.Playback
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager, IMediaEncoder mediaEncoder) public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager, IMediaEncoder mediaEncoder, IUserManager userManager)
{ {
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager; _deviceManager = deviceManager;
@ -77,6 +79,7 @@ namespace MediaBrowser.Api.Playback
_config = config; _config = config;
_networkManager = networkManager; _networkManager = networkManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_userManager = userManager;
} }
public object Get(GetBitrateTestBytes request) public object Get(GetBitrateTestBytes request)
@ -119,7 +122,7 @@ namespace MediaBrowser.Api.Playback
SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate, SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex, request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
request.SubtitleStreamIndex, request.PlaySessionId); request.SubtitleStreamIndex, request.PlaySessionId, request.UserId);
} }
else else
{ {
@ -159,7 +162,7 @@ namespace MediaBrowser.Api.Playback
{ {
var mediaSourceId = request.MediaSourceId; var mediaSourceId = request.MediaSourceId;
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex); SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.UserId);
} }
return ToOptimizedResult(info); return ToOptimizedResult(info);
@ -221,13 +224,14 @@ namespace MediaBrowser.Api.Playback
long startTimeTicks, long startTimeTicks,
string mediaSourceId, string mediaSourceId,
int? audioStreamIndex, int? audioStreamIndex,
int? subtitleStreamIndex) int? subtitleStreamIndex,
string userId)
{ {
var item = _libraryManager.GetItemById(itemId); var item = _libraryManager.GetItemById(itemId);
foreach (var mediaSource in result.MediaSources) foreach (var mediaSource in result.MediaSources)
{ {
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, result.PlaySessionId); SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, result.PlaySessionId, userId);
} }
SortMediaSources(result, maxBitrate); SortMediaSources(result, maxBitrate);
@ -242,7 +246,8 @@ namespace MediaBrowser.Api.Playback
string mediaSourceId, string mediaSourceId,
int? audioStreamIndex, int? audioStreamIndex,
int? subtitleStreamIndex, int? subtitleStreamIndex,
string playSessionId) string playSessionId,
string userId)
{ {
var streamBuilder = new StreamBuilder(_mediaEncoder, Logger); var streamBuilder = new StreamBuilder(_mediaEncoder, Logger);
@ -262,6 +267,8 @@ namespace MediaBrowser.Api.Playback
options.SubtitleStreamIndex = subtitleStreamIndex; options.SubtitleStreamIndex = subtitleStreamIndex;
} }
var user = _userManager.GetUserById(userId);
if (mediaSource.SupportsDirectPlay) if (mediaSource.SupportsDirectPlay)
{ {
var supportsDirectStream = mediaSource.SupportsDirectStream; var supportsDirectStream = mediaSource.SupportsDirectStream;
@ -270,6 +277,14 @@ namespace MediaBrowser.Api.Playback
mediaSource.SupportsDirectStream = true; mediaSource.SupportsDirectStream = true;
options.MaxBitrate = maxBitrate; options.MaxBitrate = maxBitrate;
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
{
options.ForceDirectPlay = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it // The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) : streamBuilder.BuildAudioItem(options) :
@ -293,6 +308,14 @@ namespace MediaBrowser.Api.Playback
{ {
options.MaxBitrate = GetMaxBitrate(maxBitrate); options.MaxBitrate = GetMaxBitrate(maxBitrate);
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
{
options.ForceDirectStream = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it // The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) : streamBuilder.BuildAudioItem(options) :

View File

@ -347,7 +347,7 @@ namespace MediaBrowser.Api.Playback.Progressive
outputHeaders[item.Key] = item.Value; outputHeaders[item.Key] = item.Value;
} }
Func<Stream,Task> streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream); Func<Stream,Task> streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream, CancellationToken.None);
return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders); return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders);
} }

View File

@ -3,88 +3,12 @@ using ServiceStack.Web;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommonIO; using CommonIO;
namespace MediaBrowser.Api.Playback.Progressive namespace MediaBrowser.Api.Playback.Progressive
{ {
public class ProgressiveStreamWriter : IStreamWriter, IHasOptions
{
private string Path { get; set; }
private ILogger Logger { get; set; }
private readonly IFileSystem _fileSystem;
private readonly TranscodingJob _job;
/// <summary>
/// The _options
/// </summary>
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
/// <summary>
/// Gets the options.
/// </summary>
/// <value>The options.</value>
public IDictionary<string, string> Options
{
get { return _options; }
}
/// <summary>
/// Initializes a new instance of the <see cref="ProgressiveStreamWriter" /> class.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem, TranscodingJob job)
{
Path = path;
Logger = logger;
_fileSystem = fileSystem;
_job = job;
}
/// <summary>
/// Writes to.
/// </summary>
/// <param name="responseStream">The response stream.</param>
public void WriteTo(Stream responseStream)
{
var task = WriteToAsync(responseStream);
Task.WaitAll(task);
}
/// <summary>
/// Writes to.
/// </summary>
/// <param name="responseStream">The response stream.</param>
public async Task WriteToAsync(Stream responseStream)
{
try
{
await new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream).ConfigureAwait(false);
}
catch (IOException)
{
// These error are always the same so don't dump the whole stack trace
Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
throw;
}
catch (Exception ex)
{
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
throw;
}
finally
{
if (_job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(_job);
}
}
}
}
public class ProgressiveFileCopier public class ProgressiveFileCopier
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -92,7 +16,7 @@ namespace MediaBrowser.Api.Playback.Progressive
private readonly ILogger _logger; private readonly ILogger _logger;
// 256k // 256k
private const int BufferSize = 262144; private const int BufferSize = 81920;
private long _bytesWritten = 0; private long _bytesWritten = 0;
@ -103,22 +27,18 @@ namespace MediaBrowser.Api.Playback.Progressive
_logger = logger; _logger = logger;
} }
public async Task StreamFile(string path, Stream outputStream) public async Task StreamFile(string path, Stream outputStream, CancellationToken cancellationToken)
{ {
var eofCount = 0; var eofCount = 0;
long position = 0;
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{ {
while (eofCount < 15) while (eofCount < 15)
{ {
await CopyToInternal(fs, outputStream, BufferSize).ConfigureAwait(false); var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false);
var fsPosition = fs.Position; //var position = fs.Position;
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
var bytesRead = fsPosition - position;
//Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path);
if (bytesRead == 0) if (bytesRead == 0)
{ {
@ -126,57 +46,36 @@ namespace MediaBrowser.Api.Playback.Progressive
{ {
eofCount++; eofCount++;
} }
await Task.Delay(100).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false);
} }
else else
{ {
eofCount = 0; eofCount = 0;
} }
position = fsPosition;
} }
} }
} }
private async Task CopyToInternal(Stream source, Stream destination, int bufferSize) private async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
{ {
var array = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
int count; int bytesRead;
while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0) int totalBytesRead = 0;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{ {
//if (_job != null) await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
//{
// var didPause = false;
// var totalPauseTime = 0;
// if (_job.IsUserPaused) _bytesWritten += bytesRead;
// { totalBytesRead += bytesRead;
// _logger.Debug("Pausing writing to network stream while user has paused playback.");
// while (_job.IsUserPaused && totalPauseTime < 30000)
// {
// didPause = true;
// var pauseTime = 500;
// totalPauseTime += pauseTime;
// await Task.Delay(pauseTime).ConfigureAwait(false);
// }
// }
// if (didPause)
// {
// _logger.Debug("Resuming writing to network stream due to user unpausing playback.");
// }
//}
await destination.WriteAsync(array, 0, count).ConfigureAwait(false);
_bytesWritten += count;
if (_job != null) if (_job != null)
{ {
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
} }
} }
return totalBytesRead;
} }
} }
} }

View File

@ -55,7 +55,7 @@
<HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath> <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath>
</Reference> </Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.3.5\lib\net45\NLog.dll</HintPath> <HintPath>..\packages\NLog.4.3.6\lib\net45\NLog.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Patterns.Logging"> <Reference Include="Patterns.Logging">

View File

@ -2,7 +2,7 @@
<packages> <packages>
<package id="CommonIO" version="1.0.0.9" targetFramework="net45" /> <package id="CommonIO" version="1.0.0.9" targetFramework="net45" />
<package id="morelinq" version="1.4.0" targetFramework="net45" /> <package id="morelinq" version="1.4.0" targetFramework="net45" />
<package id="NLog" version="4.3.5" targetFramework="net45" /> <package id="NLog" version="4.3.6" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" /> <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
<package id="SimpleInjector" version="3.2.0" targetFramework="net45" /> <package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
</packages> </packages>

View File

@ -9,11 +9,11 @@ namespace MediaBrowser.Common.IO
/// <summary> /// <summary>
/// The default copy to buffer size /// The default copy to buffer size
/// </summary> /// </summary>
public const int DefaultCopyToBufferSize = 262144; public const int DefaultCopyToBufferSize = 81920;
/// <summary> /// <summary>
/// The default file stream buffer size /// The default file stream buffer size
/// </summary> /// </summary>
public const int DefaultFileStreamBufferSize = 262144; public const int DefaultFileStreamBufferSize = 81920;
} }
} }

View File

@ -46,6 +46,12 @@ namespace MediaBrowser.Controller.Entities.Audio
} }
} }
[IgnoreDataMember]
public override bool EnableForceSaveOnDateModifiedChange
{
get { return true; }
}
public Audio() public Audio()
{ {
Artists = new List<string>(); Artists = new List<string>();

View File

@ -455,7 +455,10 @@ namespace MediaBrowser.Controller.Entities
public DateTime DateLastRefreshed { get; set; } public DateTime DateLastRefreshed { get; set; }
[IgnoreDataMember] [IgnoreDataMember]
public DateTime? DateModifiedDuringLastRefresh { get; set; } public virtual bool EnableForceSaveOnDateModifiedChange
{
get { return false; }
}
/// <summary> /// <summary>
/// The logger /// The logger

View File

@ -34,6 +34,12 @@ namespace MediaBrowser.Controller.Entities
return SeriesName; return SeriesName;
} }
[IgnoreDataMember]
public override bool EnableForceSaveOnDateModifiedChange
{
get { return true; }
}
public Guid? FindSeriesId() public Guid? FindSeriesId()
{ {
return SeriesId; return SeriesId;

View File

@ -373,13 +373,6 @@ namespace MediaBrowser.Controller.Entities
if (currentChildren.TryGetValue(child.Id, out currentChild) && IsValidFromResolver(currentChild, child)) if (currentChildren.TryGetValue(child.Id, out currentChild) && IsValidFromResolver(currentChild, child))
{ {
var currentChildLocationType = currentChild.LocationType;
if (currentChildLocationType != LocationType.Remote &&
currentChildLocationType != LocationType.Virtual)
{
currentChild.DateModified = child.DateModified;
}
await UpdateIsOffline(currentChild, false).ConfigureAwait(false); await UpdateIsOffline(currentChild, false).ConfigureAwait(false);
validChildren.Add(currentChild); validChildren.Add(currentChild);

View File

@ -4,6 +4,7 @@ using MediaBrowser.Model.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization;
namespace MediaBrowser.Controller.Entities namespace MediaBrowser.Controller.Entities
{ {
@ -32,6 +33,12 @@ namespace MediaBrowser.Controller.Entities
locationType != LocationType.Virtual; locationType != LocationType.Virtual;
} }
[IgnoreDataMember]
public override bool EnableForceSaveOnDateModifiedChange
{
get { return true; }
}
/// <summary> /// <summary>
/// Gets or sets the remote trailers. /// Gets or sets the remote trailers.
/// </summary> /// </summary>
@ -42,6 +49,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets the type of the media. /// Gets the type of the media.
/// </summary> /// </summary>
/// <value>The type of the media.</value> /// <value>The type of the media.</value>
[IgnoreDataMember]
public override string MediaType public override string MediaType
{ {
get { return Model.Entities.MediaType.Game; } get { return Model.Entities.MediaType.Game; }

View File

@ -207,8 +207,6 @@ namespace MediaBrowser.Controller.Entities
/// <param name="image">The image.</param> /// <param name="image">The image.</param>
/// <param name="index">The index.</param> /// <param name="index">The index.</param>
void SetImage(ItemImageInfo image, int index); void SetImage(ItemImageInfo image, int index);
DateTime? DateModifiedDuringLastRefresh { get; set; }
} }
public static class HasImagesExtensions public static class HasImagesExtensions

View File

@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets the date modified. /// Gets the date modified.
/// </summary> /// </summary>
/// <value>The date modified.</value> /// <value>The date modified.</value>
DateTime DateModified { get; } DateTime DateModified { get; set; }
/// <summary> /// <summary>
/// Gets or sets the date last saved. /// Gets or sets the date last saved.
@ -51,5 +51,7 @@ namespace MediaBrowser.Controller.Entities
bool SupportsPeople { get; } bool SupportsPeople { get; }
bool RequiresRefresh(); bool RequiresRefresh();
bool EnableForceSaveOnDateModifiedChange { get; }
} }
} }

View File

@ -51,6 +51,12 @@ namespace MediaBrowser.Controller.Entities
} }
} }
[IgnoreDataMember]
public override bool EnableForceSaveOnDateModifiedChange
{
get { return true; }
}
public override bool CanDownload() public override bool CanDownload()
{ {
return true; return true;

View File

@ -58,6 +58,12 @@ namespace MediaBrowser.Controller.Entities
} }
} }
[IgnoreDataMember]
public override bool EnableForceSaveOnDateModifiedChange
{
get { return true; }
}
public int? TotalBitrate { get; set; } public int? TotalBitrate { get; set; }
public ExtraType? ExtraType { get; set; } public ExtraType? ExtraType { get; set; }

View File

@ -102,6 +102,12 @@ namespace MediaBrowser.Controller.Providers
{ {
var directory = Path.GetDirectoryName(path); var directory = Path.GetDirectoryName(path);
if (string.IsNullOrWhiteSpace(directory))
{
_logger.Debug("Parent path is null for {0}", path);
return null;
}
var dict = GetFileSystemDictionary(directory, false); var dict = GetFileSystemDictionary(directory, false);
FileSystemMetadata entry; FileSystemMetadata entry;

View File

@ -346,8 +346,8 @@
<Compile Include="..\MediaBrowser.Model\Dlna\HttpHeaderInfo.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\HttpHeaderInfo.cs">
<Link>Dlna\HttpHeaderInfo.cs</Link> <Link>Dlna\HttpHeaderInfo.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\ILocalPlayer.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\ITranscoderSupport.cs">
<Link>Dlna\ILocalPlayer.cs</Link> <Link>Dlna\ITranscoderSupport.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfile.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfile.cs">
<Link>Dlna\MediaFormatProfile.cs</Link> <Link>Dlna\MediaFormatProfile.cs</Link>
@ -355,9 +355,6 @@
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs">
<Link>Dlna\MediaFormatProfileResolver.cs</Link> <Link>Dlna\MediaFormatProfileResolver.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\NullLocalPlayer.cs">
<Link>Dlna\NullLocalPlayer.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs">
<Link>Dlna\PlaybackErrorCode.cs</Link> <Link>Dlna\PlaybackErrorCode.cs</Link>
</Compile> </Compile>

View File

@ -318,8 +318,8 @@
<Compile Include="..\MediaBrowser.Model\Dlna\HttpHeaderInfo.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\HttpHeaderInfo.cs">
<Link>Dlna\HttpHeaderInfo.cs</Link> <Link>Dlna\HttpHeaderInfo.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\ILocalPlayer.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\ITranscoderSupport.cs">
<Link>Dlna\ILocalPlayer.cs</Link> <Link>Dlna\ITranscoderSupport.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfile.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfile.cs">
<Link>Dlna\MediaFormatProfile.cs</Link> <Link>Dlna\MediaFormatProfile.cs</Link>
@ -327,9 +327,6 @@
<Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\MediaFormatProfileResolver.cs">
<Link>Dlna\MediaFormatProfileResolver.cs</Link> <Link>Dlna\MediaFormatProfileResolver.cs</Link>
</Compile> </Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\NullLocalPlayer.cs">
<Link>Dlna\NullLocalPlayer.cs</Link>
</Compile>
<Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs"> <Compile Include="..\MediaBrowser.Model\Dlna\PlaybackErrorCode.cs">
<Link>Dlna\PlaybackErrorCode.cs</Link> <Link>Dlna\PlaybackErrorCode.cs</Link>
</Compile> </Compile>

View File

@ -18,6 +18,8 @@ namespace MediaBrowser.Model.Dlna
public bool EnableDirectPlay { get; set; } public bool EnableDirectPlay { get; set; }
public bool EnableDirectStream { get; set; } public bool EnableDirectStream { get; set; }
public bool ForceDirectPlay { get; set; }
public bool ForceDirectStream { get; set; }
public string ItemId { get; set; } public string ItemId { get; set; }
public List<MediaSourceInfo> MediaSources { get; set; } public List<MediaSourceInfo> MediaSources { get; set; }

View File

@ -1,39 +0,0 @@

namespace MediaBrowser.Model.Dlna
{
public interface ILocalPlayer
{
/// <summary>
/// Determines whether this instance [can access file] the specified path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if this instance [can access file] the specified path; otherwise, <c>false</c>.</returns>
bool CanAccessFile(string path);
/// <summary>
/// Determines whether this instance [can access directory] the specified path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if this instance [can access directory] the specified path; otherwise, <c>false</c>.</returns>
bool CanAccessDirectory(string path);
/// <summary>
/// Determines whether this instance [can access URL] the specified URL.
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="requiresCustomRequestHeaders">if set to <c>true</c> [requires custom request headers].</param>
/// <returns><c>true</c> if this instance [can access URL] the specified URL; otherwise, <c>false</c>.</returns>
bool CanAccessUrl(string url, bool requiresCustomRequestHeaders);
}
public interface ITranscoderSupport
{
bool CanEncodeToAudioCodec(string codec);
}
public class FullTranscoderSupport : ITranscoderSupport
{
public bool CanEncodeToAudioCodec(string codec)
{
return true;
}
}
}

View File

@ -0,0 +1,15 @@
namespace MediaBrowser.Model.Dlna
{
public interface ITranscoderSupport
{
bool CanEncodeToAudioCodec(string codec);
}
public class FullTranscoderSupport : ITranscoderSupport
{
public bool CanEncodeToAudioCodec(string codec)
{
return true;
}
}
}

View File

@ -1,21 +0,0 @@

namespace MediaBrowser.Model.Dlna
{
public class NullLocalPlayer : ILocalPlayer
{
public bool CanAccessFile(string path)
{
return false;
}
public bool CanAccessDirectory(string path)
{
return false;
}
public bool CanAccessUrl(string url, bool requiresCustomRequestHeaders)
{
return false;
}
}
}

View File

@ -11,29 +11,17 @@ namespace MediaBrowser.Model.Dlna
{ {
public class StreamBuilder public class StreamBuilder
{ {
private readonly ILocalPlayer _localPlayer;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ITranscoderSupport _transcoderSupport; private readonly ITranscoderSupport _transcoderSupport;
public StreamBuilder(ILocalPlayer localPlayer, ITranscoderSupport transcoderSupport, ILogger logger) public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
{ {
_transcoderSupport = transcoderSupport; _transcoderSupport = transcoderSupport;
_localPlayer = localPlayer;
_logger = logger; _logger = logger;
} }
public StreamBuilder(ITranscoderSupport transcoderSupport, ILogger logger)
: this(new NullLocalPlayer(), transcoderSupport, logger)
{
}
public StreamBuilder(ILocalPlayer localPlayer, ILogger logger)
: this(localPlayer, new FullTranscoderSupport(), logger)
{
}
public StreamBuilder(ILogger logger) public StreamBuilder(ILogger logger)
: this(new NullLocalPlayer(), new FullTranscoderSupport(), logger) : this(new FullTranscoderSupport(), logger)
{ {
} }
@ -127,6 +115,20 @@ namespace MediaBrowser.Model.Dlna
DeviceProfile = options.Profile DeviceProfile = options.Profile
}; };
if (options.ForceDirectPlay)
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
playlistItem.Container = item.Container;
return playlistItem;
}
if (options.ForceDirectStream)
{
playlistItem.PlayMethod = PlayMethod.DirectStream;
playlistItem.Container = item.Container;
return playlistItem;
}
MediaStream audioStream = item.GetDefaultAudioStream(null); MediaStream audioStream = item.GetDefaultAudioStream(null);
List<PlayMethod> directPlayMethods = GetAudioDirectPlayMethods(item, audioStream, options); List<PlayMethod> directPlayMethods = GetAudioDirectPlayMethods(item, audioStream, options);
@ -182,19 +184,7 @@ namespace MediaBrowser.Model.Dlna
if (all) if (all)
{ {
if (item.Protocol == MediaProtocol.File && if (directPlayMethods.Contains(PlayMethod.DirectStream))
directPlayMethods.Contains(PlayMethod.DirectPlay) &&
_localPlayer.CanAccessFile(item.Path))
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
}
else if (item.Protocol == MediaProtocol.Http &&
directPlayMethods.Contains(PlayMethod.DirectPlay) &&
_localPlayer.CanAccessUrl(item.Path, item.RequiredHttpHeaders.Count > 0))
{
playlistItem.PlayMethod = PlayMethod.DirectPlay;
}
else if (directPlayMethods.Contains(PlayMethod.DirectStream))
{ {
playlistItem.PlayMethod = PlayMethod.DirectStream; playlistItem.PlayMethod = PlayMethod.DirectStream;
} }
@ -413,8 +403,8 @@ namespace MediaBrowser.Model.Dlna
MediaStream videoStream = item.VideoStream; MediaStream videoStream = item.VideoStream;
// TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough // TODO: This doesn't accout for situation of device being able to handle media bitrate, but wifi connection not fast enough
bool isEligibleForDirectPlay = options.EnableDirectPlay && IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options, PlayMethod.DirectPlay); bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options), subtitleStream, options, PlayMethod.DirectPlay));
bool isEligibleForDirectStream = options.EnableDirectStream && IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options, PlayMethod.DirectStream); bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || IsEligibleForDirectPlay(item, options.GetMaxBitrate(), subtitleStream, options, PlayMethod.DirectStream));
_logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}", _logger.Info("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile", options.Profile.Name ?? "Unknown Profile",
@ -425,7 +415,7 @@ namespace MediaBrowser.Model.Dlna
if (isEligibleForDirectPlay || isEligibleForDirectStream) if (isEligibleForDirectPlay || isEligibleForDirectStream)
{ {
// See if it can be direct played // See if it can be direct played
PlayMethod? directPlay = GetVideoDirectPlayProfile(options.Profile, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream); PlayMethod? directPlay = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectPlay, isEligibleForDirectStream);
if (directPlay != null) if (directPlay != null)
{ {
@ -645,13 +635,24 @@ namespace MediaBrowser.Model.Dlna
return Math.Min(defaultBitrate, encoderAudioBitrateLimit); return Math.Min(defaultBitrate, encoderAudioBitrateLimit);
} }
private PlayMethod? GetVideoDirectPlayProfile(DeviceProfile profile, private PlayMethod? GetVideoDirectPlayProfile(VideoOptions options,
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
MediaStream videoStream, MediaStream videoStream,
MediaStream audioStream, MediaStream audioStream,
bool isEligibleForDirectPlay, bool isEligibleForDirectPlay,
bool isEligibleForDirectStream) bool isEligibleForDirectStream)
{ {
DeviceProfile profile = options.Profile;
if (options.ForceDirectPlay)
{
return PlayMethod.DirectPlay;
}
if (options.ForceDirectStream)
{
return PlayMethod.DirectStream;
}
if (videoStream == null) if (videoStream == null)
{ {
_logger.Info("Profile: {0}, Cannot direct stream with no known video stream. Path: {1}", _logger.Info("Profile: {0}, Cannot direct stream with no known video stream. Path: {1}",
@ -829,25 +830,6 @@ namespace MediaBrowser.Model.Dlna
} }
} }
if (isEligibleForDirectPlay && mediaSource.SupportsDirectPlay)
{
if (mediaSource.Protocol == MediaProtocol.Http)
{
if (_localPlayer.CanAccessUrl(mediaSource.Path, mediaSource.RequiredHttpHeaders.Count > 0))
{
return PlayMethod.DirectPlay;
}
}
else if (mediaSource.Protocol == MediaProtocol.File)
{
if (_localPlayer.CanAccessFile(mediaSource.Path))
{
return PlayMethod.DirectPlay;
}
}
}
if (isEligibleForDirectStream && mediaSource.SupportsDirectStream) if (isEligibleForDirectStream && mediaSource.SupportsDirectStream)
{ {
return PlayMethod.DirectStream; return PlayMethod.DirectStream;

View File

@ -118,9 +118,8 @@
<Compile Include="Devices\DeviceInfo.cs" /> <Compile Include="Devices\DeviceInfo.cs" />
<Compile Include="Devices\DevicesOptions.cs" /> <Compile Include="Devices\DevicesOptions.cs" />
<Compile Include="Dlna\EncodingContext.cs" /> <Compile Include="Dlna\EncodingContext.cs" />
<Compile Include="Dlna\ILocalPlayer.cs" /> <Compile Include="Dlna\ITranscoderSupport.cs" />
<Compile Include="Dlna\StreamInfoSorter.cs" /> <Compile Include="Dlna\StreamInfoSorter.cs" />
<Compile Include="Dlna\NullLocalPlayer.cs" />
<Compile Include="Dlna\PlaybackErrorCode.cs" /> <Compile Include="Dlna\PlaybackErrorCode.cs" />
<Compile Include="Dlna\PlaybackException.cs" /> <Compile Include="Dlna\PlaybackException.cs" />
<Compile Include="Dlna\ResolutionConfiguration.cs" /> <Compile Include="Dlna\ResolutionConfiguration.cs" />

View File

@ -143,6 +143,22 @@ namespace MediaBrowser.Providers.Manager
var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh, updateType).ConfigureAwait(false); var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh, updateType).ConfigureAwait(false);
updateType = updateType | beforeSaveResult; updateType = updateType | beforeSaveResult;
if (item.LocationType == LocationType.FileSystem)
{
var file = refreshOptions.DirectoryService.GetFile(item.Path);
if (file != null)
{
var fileLastWriteTime = file.LastWriteTimeUtc;
if (item.EnableForceSaveOnDateModifiedChange && fileLastWriteTime != item.DateModified)
{
Logger.Debug("Date modified for {0}. Old date {1} new date {2} Id {3}", item.Path, item.DateModified, fileLastWriteTime, item.Id);
requiresRefresh = true;
}
item.DateModified = fileLastWriteTime;
}
}
// Save if changes were made, or it's never been saved before // Save if changes were made, or it's never been saved before
if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh) if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
{ {
@ -155,12 +171,10 @@ namespace MediaBrowser.Providers.Manager
if (hasRefreshedMetadata && hasRefreshedImages) if (hasRefreshedMetadata && hasRefreshedImages)
{ {
item.DateLastRefreshed = DateTime.UtcNow; item.DateLastRefreshed = DateTime.UtcNow;
item.DateModifiedDuringLastRefresh = item.DateModified;
} }
else else
{ {
item.DateLastRefreshed = default(DateTime); item.DateLastRefreshed = default(DateTime);
item.DateModifiedDuringLastRefresh = null;
} }
// Save to database // Save to database

View File

@ -167,9 +167,10 @@ namespace MediaBrowser.Providers.MediaInfo
public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{ {
if (item.DateModifiedDuringLastRefresh.HasValue) var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{ {
return item.DateModifiedDuringLastRefresh.Value != item.DateModified; return true;
} }
return false; return false;

View File

@ -171,12 +171,10 @@ namespace MediaBrowser.Providers.MediaInfo
public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{ {
if (item.DateModifiedDuringLastRefresh.HasValue) var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{ {
if (item.DateModifiedDuringLastRefresh.Value != item.DateModified) return true;
{
return true;
}
} }
if (item.SupportsLocalMetadata) if (item.SupportsLocalMetadata)

View File

@ -195,12 +195,10 @@ namespace MediaBrowser.Providers.MediaInfo
public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{ {
if (item.DateModifiedDuringLastRefresh.HasValue) var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{ {
if (item.DateModifiedDuringLastRefresh.Value != item.DateModified) return true;
{
return true;
}
} }
return false; return false;

View File

@ -199,7 +199,6 @@ namespace MediaBrowser.Providers.Movies
var ourRelease = releases.FirstOrDefault(c => c.iso_3166_1.Equals(preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var ourRelease = releases.FirstOrDefault(c => c.iso_3166_1.Equals(preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = releases.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)); var usRelease = releases.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase));
var minimunRelease = releases.OrderBy(c => c.release_date).FirstOrDefault();
if (ourRelease != null) if (ourRelease != null)
{ {
@ -210,10 +209,6 @@ namespace MediaBrowser.Providers.Movies
{ {
movie.OfficialRating = usRelease.certification; movie.OfficialRating = usRelease.certification;
} }
else if (minimunRelease != null)
{
movie.OfficialRating = minimunRelease.iso_3166_1 + "-" + minimunRelease.certification;
}
} }
if (!string.IsNullOrWhiteSpace(movieData.release_date)) if (!string.IsNullOrWhiteSpace(movieData.release_date))

View File

@ -154,9 +154,10 @@ namespace MediaBrowser.Providers.Photos
public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
{ {
if (item.DateModifiedDuringLastRefresh.HasValue) var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{ {
return item.DateModifiedDuringLastRefresh.Value != item.DateModified; return true;
} }
return false; return false;

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Configuration; using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -6,12 +7,42 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using CommonIO; using CommonIO;
namespace MediaBrowser.Providers.TV namespace MediaBrowser.Providers.TV
{ {
public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo> public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
{ {
protected override async Task<ItemUpdateType> BeforeSave(Episode item, bool isFullRefresh, ItemUpdateType currentUpdateType)
{
var updateType = await base.BeforeSave(item, isFullRefresh, currentUpdateType).ConfigureAwait(false);
if (updateType <= ItemUpdateType.None)
{
if (!string.Equals(item.SeriesName, item.FindSeriesName(), StringComparison.Ordinal))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
if (updateType <= ItemUpdateType.None)
{
if (!string.Equals(item.SeriesSortName, item.FindSeriesSortName(), StringComparison.Ordinal))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
if (updateType <= ItemUpdateType.None)
{
if (!string.Equals(item.SeasonName, item.FindSeasonName(), StringComparison.Ordinal))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
return updateType;
}
protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings) protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
{ {
ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);

View File

@ -35,6 +35,21 @@ namespace MediaBrowser.Providers.TV
updateType |= SaveIsVirtualItem(item, episodes); updateType |= SaveIsVirtualItem(item, episodes);
} }
if (updateType <= ItemUpdateType.None)
{
if (!string.Equals(item.SeriesName, item.FindSeriesName(), StringComparison.Ordinal))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
if (updateType <= ItemUpdateType.None)
{
if (!string.Equals(item.SeriesSortName, item.FindSeriesSortName(), StringComparison.Ordinal))
{
updateType |= ItemUpdateType.MetadataImport;
}
}
return updateType; return updateType;
} }

View File

@ -28,8 +28,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
public Action OnComplete { get; set; } public Action OnComplete { get; set; }
private readonly ILogger _logger; private readonly ILogger _logger;
// 256k private const int BufferSize = 81920;
private const int BufferSize = 262144;
/// <summary> /// <summary>
/// The _options /// The _options

View File

@ -75,8 +75,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
{ {
} }
// 256k private const int BufferSize = 81920;
private const int BufferSize = 262144;
/// <summary> /// <summary>
/// Writes to. /// Writes to.

View File

@ -44,7 +44,6 @@ namespace MediaBrowser.Server.Implementations.Library
// Make sure DateCreated and DateModified have values // Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFile(item.Path); var fileInfo = directoryService.GetFile(item.Path);
item.DateModified = fileSystem.GetLastWriteTimeUtc(fileInfo);
SetDateCreated(item, fileSystem, fileInfo); SetDateCreated(item, fileSystem, fileInfo);
EnsureName(item, fileInfo); EnsureName(item, fileInfo);
@ -80,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Library
item.GetParents().Any(i => i.IsLocked); item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values // Make sure DateCreated and DateModified have values
EnsureDates(fileSystem, item, args, true); EnsureDates(fileSystem, item, args);
} }
/// <summary> /// <summary>
@ -125,8 +124,7 @@ namespace MediaBrowser.Server.Implementations.Library
/// <param name="fileSystem">The file system.</param> /// <param name="fileSystem">The file system.</param>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <param name="includeCreationTime">if set to <c>true</c> [include creation time].</param> private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args, bool includeCreationTime)
{ {
if (fileSystem == null) if (fileSystem == null)
{ {
@ -148,12 +146,7 @@ namespace MediaBrowser.Server.Implementations.Library
if (childData != null) if (childData != null)
{ {
if (includeCreationTime) SetDateCreated(item, fileSystem, childData);
{
SetDateCreated(item, fileSystem, childData);
}
item.DateModified = fileSystem.GetLastWriteTimeUtc(childData);
} }
else else
{ {
@ -161,21 +154,13 @@ namespace MediaBrowser.Server.Implementations.Library
if (fileData.Exists) if (fileData.Exists)
{ {
if (includeCreationTime) SetDateCreated(item, fileSystem, fileData);
{
SetDateCreated(item, fileSystem, fileData);
}
item.DateModified = fileSystem.GetLastWriteTimeUtc(fileData);
} }
} }
} }
else else
{ {
if (includeCreationTime) SetDateCreated(item, fileSystem, args.FileInfo);
{
SetDateCreated(item, fileSystem, args.FileInfo);
}
item.DateModified = fileSystem.GetLastWriteTimeUtc(args.FileInfo);
} }
} }

View File

@ -56,8 +56,8 @@
<Reference Include="Interfaces.IO"> <Reference Include="Interfaces.IO">
<HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath>
</Reference> </Reference>
<Reference Include="MediaBrowser.Naming, Version=1.0.6046.32295, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="MediaBrowser.Naming, Version=1.0.6046.41603, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MediaBrowser.Naming.1.0.0.53\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> <HintPath>..\packages\MediaBrowser.Naming.1.0.0.54\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="MoreLinq"> <Reference Include="MoreLinq">

View File

@ -257,7 +257,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
_connection.AddColumn(Logger, "TypedBaseItems", "TrailerTypes", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "TrailerTypes", "Text");
_connection.AddColumn(Logger, "TypedBaseItems", "CriticRating", "Float"); _connection.AddColumn(Logger, "TypedBaseItems", "CriticRating", "Float");
_connection.AddColumn(Logger, "TypedBaseItems", "CriticRatingSummary", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "CriticRatingSummary", "Text");
_connection.AddColumn(Logger, "TypedBaseItems", "DateModifiedDuringLastRefresh", "DATETIME");
_connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text");
_connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text");
_connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text");
@ -402,7 +401,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
"Tags", "Tags",
"SourceType", "SourceType",
"TrailerTypes", "TrailerTypes",
"DateModifiedDuringLastRefresh",
"OriginalTitle", "OriginalTitle",
"PrimaryVersionId", "PrimaryVersionId",
"DateLastMediaAdded", "DateLastMediaAdded",
@ -523,7 +521,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
"TrailerTypes", "TrailerTypes",
"CriticRating", "CriticRating",
"CriticRatingSummary", "CriticRatingSummary",
"DateModifiedDuringLastRefresh",
"InheritedTags", "InheritedTags",
"CleanName", "CleanName",
"PresentationUniqueKey", "PresentationUniqueKey",
@ -902,15 +899,6 @@ namespace MediaBrowser.Server.Implementations.Persistence
_saveItemCommand.GetParameter(index++).Value = item.CriticRating; _saveItemCommand.GetParameter(index++).Value = item.CriticRating;
_saveItemCommand.GetParameter(index++).Value = item.CriticRatingSummary; _saveItemCommand.GetParameter(index++).Value = item.CriticRatingSummary;
if (!item.DateModifiedDuringLastRefresh.HasValue || item.DateModifiedDuringLastRefresh.Value == default(DateTime))
{
_saveItemCommand.GetParameter(index++).Value = null;
}
else
{
_saveItemCommand.GetParameter(index++).Value = item.DateModifiedDuringLastRefresh.Value;
}
var inheritedTags = item.GetInheritedTags(); var inheritedTags = item.GetInheritedTags();
if (inheritedTags.Count > 0) if (inheritedTags.Count > 0)
{ {
@ -1370,88 +1358,101 @@ namespace MediaBrowser.Server.Implementations.Persistence
} }
} }
if (!reader.IsDBNull(51)) var index = 51;
{
item.DateModifiedDuringLastRefresh = reader.GetDateTime(51).ToUniversalTime();
}
if (!reader.IsDBNull(52)) if (!reader.IsDBNull(index))
{ {
item.OriginalTitle = reader.GetString(52); item.OriginalTitle = reader.GetString(index);
} }
index++;
var video = item as Video; var video = item as Video;
if (video != null) if (video != null)
{ {
if (!reader.IsDBNull(53)) if (!reader.IsDBNull(index))
{ {
video.PrimaryVersionId = reader.GetString(53); video.PrimaryVersionId = reader.GetString(index);
} }
} }
index++;
var folder = item as Folder; var folder = item as Folder;
if (folder != null && !reader.IsDBNull(54)) if (folder != null && !reader.IsDBNull(index))
{ {
folder.DateLastMediaAdded = reader.GetDateTime(54).ToUniversalTime(); folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime();
} }
index++;
if (!reader.IsDBNull(55)) if (!reader.IsDBNull(index))
{ {
item.Album = reader.GetString(55); item.Album = reader.GetString(index);
} }
index++;
if (!reader.IsDBNull(56)) if (!reader.IsDBNull(index))
{ {
item.CriticRating = reader.GetFloat(56); item.CriticRating = reader.GetFloat(index);
} }
index++;
if (!reader.IsDBNull(57)) if (!reader.IsDBNull(index))
{ {
item.CriticRatingSummary = reader.GetString(57); item.CriticRatingSummary = reader.GetString(index);
} }
index++;
if (!reader.IsDBNull(58)) if (!reader.IsDBNull(index))
{ {
item.IsVirtualItem = reader.GetBoolean(58); item.IsVirtualItem = reader.GetBoolean(index);
} }
index++;
var hasSeries = item as IHasSeries; var hasSeries = item as IHasSeries;
if (hasSeries != null) if (hasSeries != null)
{ {
if (!reader.IsDBNull(59)) if (!reader.IsDBNull(index))
{ {
hasSeries.SeriesName = reader.GetString(59); hasSeries.SeriesName = reader.GetString(index);
} }
} }
index++;
var episode = item as Episode; var episode = item as Episode;
if (episode != null) if (episode != null)
{ {
if (!reader.IsDBNull(60)) if (!reader.IsDBNull(index))
{ {
episode.SeasonName = reader.GetString(60); episode.SeasonName = reader.GetString(index);
} }
if (!reader.IsDBNull(61)) index++;
if (!reader.IsDBNull(index))
{ {
episode.SeasonId = reader.GetGuid(61); episode.SeasonId = reader.GetGuid(index);
} }
} }
else
{
index++;
}
index++;
if (hasSeries != null) if (hasSeries != null)
{ {
if (!reader.IsDBNull(62)) if (!reader.IsDBNull(index))
{ {
hasSeries.SeriesId = reader.GetGuid(62); hasSeries.SeriesId = reader.GetGuid(index);
} }
} }
index++;
if (hasSeries != null) if (hasSeries != null)
{ {
if (!reader.IsDBNull(63)) if (!reader.IsDBNull(index))
{ {
hasSeries.SeriesSortName = reader.GetString(63); hasSeries.SeriesSortName = reader.GetString(index);
} }
} }
index++;
return item; return item;
} }

View File

@ -119,12 +119,29 @@ namespace MediaBrowser.Server.Implementations.TV
// Avoid implicitly captured closure // Avoid implicitly captured closure
var currentUser = user; var currentUser = user;
return series var allNextUp = series
.Select(i => GetNextUp(i, currentUser)) .Select(i => GetNextUp(i, currentUser))
.Where(i => i.Item1 != null)
// Include if an episode was found, and either the series is not unwatched or the specific series was requested // Include if an episode was found, and either the series is not unwatched or the specific series was requested
.Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId)))
.OrderByDescending(i => i.Item2) .OrderByDescending(i => i.Item2)
.ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue) .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue)
.ToList();
// If viewing all next up for all series, remove first episodes
if (string.IsNullOrWhiteSpace(request.SeriesId))
{
var withoutFirstEpisode = allNextUp
.Where(i => !i.Item3)
.ToList();
// But if that returns empty, keep those first episodes (avoid completely empty view)
if (withoutFirstEpisode.Count > 0)
{
allNextUp = withoutFirstEpisode;
}
}
return allNextUp
.Select(i => i.Item1) .Select(i => i.Item1)
.Take(request.Limit ?? int.MaxValue); .Take(request.Limit ?? int.MaxValue);
} }

View File

@ -4,7 +4,7 @@
<package id="Emby.XmlTv" version="1.0.0.55" targetFramework="net45" /> <package id="Emby.XmlTv" version="1.0.0.55" targetFramework="net45" />
<package id="ini-parser" version="2.3.0" targetFramework="net45" /> <package id="ini-parser" version="2.3.0" targetFramework="net45" />
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" /> <package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
<package id="MediaBrowser.Naming" version="1.0.0.53" targetFramework="net45" /> <package id="MediaBrowser.Naming" version="1.0.0.54" targetFramework="net45" />
<package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" /> <package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
<package id="morelinq" version="1.4.0" targetFramework="net45" /> <package id="morelinq" version="1.4.0" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" /> <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- DWXMLSource="StringCheckSample.xml" -->
<!DOCTYPE xsl:stylesheet [
<!ENTITY nbsp "&#160;">
<!ENTITY copy "&#169;">
<!ENTITY reg "&#174;">
<!ENTITY trade "&#8482;">
<!ENTITY mdash "&#8212;">
<!ENTITY ldquo "&#8220;">
<!ENTITY rdquo "&#8221;">
<!ENTITY pound "&#163;">
<!ENTITY yen "&#165;">
<!ENTITY euro "&#8364;">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>
<xsl:value-of select="StringUsages/@ReportTitle"/>
</title>
<style>
body {
background: #F3F3F4;
color: #1E1E1F;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
padding: 0;
margin: 0;
}
h1 {
padding: 10px 0px 10px 10px;
font-size: 21pt;
background-color: #E2E2E2;
border-bottom: 1px #C1C1C2 solid;
color: #201F20;
margin: 0;
font-weight: normal;
}
h2 {
font-size: 18pt;
font-weight: normal;
padding: 15px 0 5px 0;
margin: 0;
}
h3 {
font-weight: normal;
font-size: 15pt;
margin: 0;
padding: 15px 0 5px 0;
background-color: transparent;
}
/* Color all hyperlinks one color */
a {
color: #1382CE;
}
/* Table styles */
table {
border-spacing: 0 0;
border-collapse: collapse;
font-size: 10pt;
}
table th {
background: #E7E7E8;
text-align: left;
text-decoration: none;
font-weight: normal;
padding: 3px 6px 3px 6px;
border: 1px solid #CBCBCB;
}
table td {
vertical-align: top;
padding: 3px 6px 5px 5px;
margin: 0px;
border: 1px solid #CBCBCB;
background: #F7F7F8;
}
/* Local link is a style for hyperlinks that link to file:/// content, there are lots so color them as 'normal' text until the user mouse overs */
.localLink {
color: #1E1E1F;
background: #EEEEED;
text-decoration: none;
}
.localLink:hover {
color: #1382CE;
background: #FFFF99;
text-decoration: none;
}
.baseCell {
width: 100%;
color: #427A9F;
}
.stringCell {
display: table;
}
.tokenCell {
white-space: nowrap;
}
.occurrence {
padding-left: 40px;
}
.block {
display: table-cell;
}
/* Padding around the content after the h1 */
#content {
padding: 0px 12px 12px 12px;
}
#messages table {
width: 97%;
}
</style>
</head>
<body>
<h1>
<xsl:value-of select="StringUsages/@ReportTitle"/>
</h1>
<div id="content">
<h2>Strings</h2>
<div id="messages">
<table>
<tbody>
<xsl:for-each select="StringUsages/Dictionary">
<tr>
<th class="baseCell"> <div class="stringCell">
<div class="block tokenCell"><strong><xsl:value-of select="@Token"/></strong>: "</div>
<div class="block"><xsl:value-of select="@Text"/>"</div>
</div></th>
</tr>
<xsl:for-each select="Occurence">
<xsl:variable name="hyperlink"><xsl:value-of select="@FullPath" /></xsl:variable>
<tr>
<td class="baseCell occurrence"><a href="{@FullPath}"><xsl:value-of select="@FileName"/>:<xsl:value-of select="@LineNumber"/></a></td>
</tr>
</xsl:for-each>
</xsl:for-each>
</tbody>
</table>
</div>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View File

@ -0,0 +1,239 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<?xml-stylesheet type="text/xsl" href="StringCheck.xslt"?>
<StringUsages Mode="All">
<Dictionary Token="LabelExit" Text="Exit" />
<Dictionary Token="LabelVisitCommunity" Text="Visit Community" />
<Dictionary Token="LabelGithub" Text="Github" />
<Dictionary Token="LabelSwagger" Text="Swagger" />
<Dictionary Token="LabelStandard" Text="Standard" />
<Dictionary Token="LabelApiDocumentation" Text="Api Documentation" />
<Dictionary Token="LabelDeveloperResources" Text="Developer Resources" />
<Dictionary Token="LabelBrowseLibrary" Text="Browse Library" />
<Dictionary Token="LabelConfigureServer" Text="Configure Emby" />
<Dictionary Token="LabelOpenLibraryViewer" Text="Open Library Viewer" />
<Dictionary Token="LabelRestartServer" Text="Restart Server" />
<Dictionary Token="LabelShowLogWindow" Text="Show Log Window" />
<Dictionary Token="LabelPrevious" Text="Previous">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="21" />
<Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="54" />
<Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="40" />
<Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="19" />
<Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="30" />
<Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="31" />
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="17" />
<Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="32" />
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="27" />
</Dictionary>
<Dictionary Token="LabelFinish" Text="Finish">
<Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="41" />
</Dictionary>
<Dictionary Token="LabelNext" Text="Next">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="22" />
<Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="55" />
<Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="20" />
<Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="31" />
<Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="32" />
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="18" />
<Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="33" />
<Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="25" />
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="28" />
</Dictionary>
<Dictionary Token="LabelYoureDone" Text="You're Done!">
<Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="7" />
</Dictionary>
<Dictionary Token="WelcomeToProject" Text="Welcome to Emby!">
<Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="10" />
</Dictionary>
<Dictionary Token="ThisWizardWillGuideYou" Text="This wizard will help guide you through the setup process. To begin, please select your preferred language.">
<Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="16" />
</Dictionary>
<Dictionary Token="TellUsAboutYourself" Text="Tell us about yourself">
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="8" />
</Dictionary>
<Dictionary Token="ButtonQuickStartGuide" Text="Quick start guide">
<Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="12" />
</Dictionary>
<Dictionary Token="LabelYourFirstName" Text="Your first name:">
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="14" />
</Dictionary>
<Dictionary Token="MoreUsersCanBeAddedLater" Text="More users can be added later within the Dashboard.">
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="15" />
</Dictionary>
<Dictionary Token="UserProfilesIntro" Text="Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls.">
<Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="11" />
</Dictionary>
<Dictionary Token="LabelWindowsService" Text="Windows Service">
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="7" />
</Dictionary>
<Dictionary Token="AWindowsServiceHasBeenInstalled" Text="A Windows Service has been installed.">
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="10" />
</Dictionary>
<Dictionary Token="WindowsServiceIntro1" Text="Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead.">
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="12" />
</Dictionary>
<Dictionary Token="WindowsServiceIntro2" Text="If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders.">
<Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="14" />
</Dictionary>
<Dictionary Token="WizardCompleted" Text="That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click &lt;b&gt;Finish&lt;/b&gt; to view the &lt;b&gt;Server Dashboard&lt;/b&gt;.">
<Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="10" />
</Dictionary>
<Dictionary Token="LabelConfigureSettings" Text="Configure settings">
<Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="8" />
</Dictionary>
<Dictionary Token="LabelEnableVideoImageExtraction" Text="Enable video image extraction" />
<Dictionary Token="VideoImageExtractionHelp" Text="For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation." />
<Dictionary Token="LabelEnableChapterImageExtractionForMovies" Text="Extract chapter image extraction for Movies" />
<Dictionary Token="LabelChapterImageExtractionForMoviesHelp" Text="Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours." />
<Dictionary Token="LabelEnableAutomaticPortMapping" Text="Enable automatic port mapping" />
<Dictionary Token="LabelEnableAutomaticPortMappingHelp" Text="UPnP allows automated router configuration for easy remote access. This may not work with some router models." />
<Dictionary Token="HeaderTermsOfService" Text="Emby Terms of Service">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="9" />
</Dictionary>
<Dictionary Token="MessagePleaseAcceptTermsOfService" Text="Please accept the terms of service and privacy policy before continuing.">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="12" />
</Dictionary>
<Dictionary Token="OptionIAcceptTermsOfService" Text="I accept the terms of service">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="17" />
</Dictionary>
<Dictionary Token="ButtonPrivacyPolicy" Text="Privacy policy">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="14" />
</Dictionary>
<Dictionary Token="ButtonTermsOfService" Text="Terms of Service">
<Occurence FileName="\wizardagreement.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardagreement.html" LineNumber="15" />
</Dictionary>
<Dictionary Token="HeaderDeveloperOptions" Text="Developer Options">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="108" />
</Dictionary>
<Dictionary Token="OptionEnableWebClientResponseCache" Text="Enable web response caching">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="112" />
</Dictionary>
<Dictionary Token="OptionDisableForDevelopmentHelp" Text="Configure these as needed for web development purposes.">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="119" />
</Dictionary>
<Dictionary Token="OptionEnableWebClientResourceMinification" Text="Enable web resource minification">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="116" />
</Dictionary>
<Dictionary Token="LabelDashboardSourcePath" Text="Web client source path:">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="124" />
</Dictionary>
<Dictionary Token="LabelDashboardSourcePathHelp" Text="If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location.">
<Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="126" />
</Dictionary>
<Dictionary Token="ButtonConvertMedia" Text="Convert media">
<Occurence FileName="\syncactivity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncactivity.html" LineNumber="22" />
</Dictionary>
<Dictionary Token="ButtonOrganize" Text="Organize">
<Occurence FileName="\autoorganizelog.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\autoorganizelog.html" LineNumber="8" />
<Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="293" />
<Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="294" />
<Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="296" />
</Dictionary>
<Dictionary Token="LinkedToEmbyConnect" Text="Linked to Emby Connect" />
<Dictionary Token="HeaderSupporterBenefits" Text="Emby Premiere Benefits" />
<Dictionary Token="HeaderAddUser" Text="Add User" />
<Dictionary Token="LabelAddConnectSupporterHelp" Text="To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page." />
<Dictionary Token="LabelPinCode" Text="Pin code:" />
<Dictionary Token="OptionHideWatchedContentFromLatestMedia" Text="Hide watched content from latest media">
<Occurence FileName="\mypreferenceshome.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mypreferenceshome.html" LineNumber="114" />
</Dictionary>
<Dictionary Token="HeaderSync" Text="Sync">
<Occurence FileName="\mysyncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mysyncsettings.html" LineNumber="7" />
<Occurence FileName="\scripts\registrationservices.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\registrationservices.js" LineNumber="175" />
<Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="82" />
</Dictionary>
<Dictionary Token="ButtonOk" Text="Ok">
<Occurence FileName="\components\directorybrowser\directorybrowser.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\directorybrowser\directorybrowser.js" LineNumber="147" />
<Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="45" />
<Occurence FileName="\components\medialibrarycreator\medialibrarycreator.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html" LineNumber="30" />
<Occurence FileName="\components\metadataeditor\personeditor.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\metadataeditor\personeditor.template.html" LineNumber="33" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="372" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="453" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="504" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="542" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="590" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="630" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="661" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="706" />
<Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="113" />
<Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="42" />
</Dictionary>
<Dictionary Token="ButtonCancel" Text="Cancel">
<Occurence FileName="\components\tvproviders\schedulesdirect.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\schedulesdirect.template.html" LineNumber="68" />
<Occurence FileName="\components\tvproviders\xmltv.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\xmltv.template.html" LineNumber="48" />
<Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="74" />
<Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="108" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="325" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="375" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="456" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="507" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="545" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="593" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="633" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="664" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="709" />
<Occurence FileName="\forgotpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpassword.html" LineNumber="23" />
<Occurence FileName="\forgotpasswordpin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpasswordpin.html" LineNumber="22" />
<Occurence FileName="\livetvseriestimer.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvseriestimer.html" LineNumber="62" />
<Occurence FileName="\livetvtunerprovider-hdhomerun.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-hdhomerun.html" LineNumber="35" />
<Occurence FileName="\livetvtunerprovider-m3u.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-m3u.html" LineNumber="19" />
<Occurence FileName="\livetvtunerprovider-satip.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-satip.html" LineNumber="65" />
<Occurence FileName="\login.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\login.html" LineNumber="27" />
<Occurence FileName="\notificationsetting.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\notificationsetting.html" LineNumber="64" />
<Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="85" />
<Occurence FileName="\scripts\librarylist.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\librarylist.js" LineNumber="349" />
<Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="167" />
<Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="436" />
<Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="43" />
<Occurence FileName="\scripts\site.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\site.js" LineNumber="1025" />
<Occurence FileName="\scripts\userprofilespage.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\userprofilespage.js" LineNumber="198" />
<Occurence FileName="\syncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncsettings.html" LineNumber="43" />
<Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="111" />
<Occurence FileName="\userlibraryaccess.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userlibraryaccess.html" LineNumber="57" />
<Occurence FileName="\usernew.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\usernew.html" LineNumber="45" />
<Occurence FileName="\userparentalcontrol.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userparentalcontrol.html" LineNumber="101" />
</Dictionary>
<Dictionary Token="ButtonExit" Text="Exit" />
<Dictionary Token="ButtonNew" Text="New">
<Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="18" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="107" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="278" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="290" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="296" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="302" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="308" />
<Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="314" />
<Occurence FileName="\dlnaprofiles.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofiles.html" LineNumber="14" />
<Occurence FileName="\serversecurity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\serversecurity.html" LineNumber="8" />
</Dictionary>
<Dictionary Token="HeaderTaskTriggers" Text="Task Triggers">
<Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="11" />
</Dictionary>
<Dictionary Token="HeaderTV" Text="TV">
<Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="113" />
</Dictionary>
<Dictionary Token="HeaderAudio" Text="Audio">
<Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="39" />
</Dictionary>
<Dictionary Token="HeaderVideo" Text="Video">
<Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="50" />
</Dictionary>
<Dictionary Token="HeaderPaths" Text="Paths">
<Occurence FileName="\dashboard.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboard.html" LineNumber="92" />
</Dictionary>
<Dictionary Token="CategorySync" Text="Sync" />
<Dictionary Token="TabPlaylist" Text="Playlist">
<Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="20" />
</Dictionary>
<Dictionary Token="HeaderEasyPinCode" Text="Easy Pin Code">
<Occurence FileName="\myprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\myprofile.html" LineNumber="69" />
<Occurence FileName="\userpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userpassword.html" LineNumber="42" />
</Dictionary>
<Dictionary Token="HeaderGrownupsOnly" Text="Grown-ups Only!" />
<Dictionary Token="DividerOr" Text="-- or --" />
<Dictionary Token="HeaderInstalledServices" Text="Installed Services">
<Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="6" />
</Dictionary>
<Dictionary Token="HeaderAvailableServices" Text="Available Services">
<Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="11" />
</Dictionary>
</StringUsages>

View File

@ -0,0 +1,260 @@
using MediaBrowser.Tests.ConsistencyTests.TextIndexing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace MediaBrowser.Tests.ConsistencyTests
{
/// <summary>
/// This class contains tests for reporting the usage of localization string tokens
/// in the dashboard-ui or similar.
/// </summary>
/// <remarks>
/// <para>Run one of the two tests using Visual Studio's "Test Explorer":</para>
/// <para>
/// <list type="bullet">
/// <item><see cref="ReportStringUsage"/></item>
/// <item><see cref="ReportUnusedStrings"/></item>
/// </list>
/// </para>
/// <para>
/// On successful run, the bottom section of the test explorer will contain a link "Output".
/// This link will open the test results, displaying the trace and two attachment links.
/// One link will open the output folder, the other link will open the output xml file.
/// </para>
/// <para>
/// The output xml file contains a stylesheet link to render the results as html.
/// How that works depends on the default application configured for XML files:
/// </para>
/// <para><list type="bullet">
/// <item><term>Visual Studio</term>
/// <description>Will open in XML source view. To view the html result, click menu
/// 'XML' => 'Start XSLT without debugging'</description></item>
/// <item><term>Internet Explorer</term>
/// <description>XSL transform will be applied automatically.</description></item>
/// <item><term>Firefox</term>
/// <description>XSL transform will be applied automatically.</description></item>
/// <item><term>Chrome</term>
/// <description>Does not work. Chrome is unable/unwilling to apply xslt transforms from local files.</description></item>
/// </list></para>
/// </remarks>
[TestClass]
public class StringUsageReporter
{
/// <summary>
/// Root path of the web application
/// </summary>
/// <remarks>
/// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
/// </remarks>
public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui";
/// <summary>
/// Path to the strings file, relative to <see cref="WebFolder"/>.
/// </summary>
public const string StringsFile = @"strings\en-US.json";
/// <summary>
/// Path to the output folder
/// </summary>
/// <remarks>
/// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
/// Important: When changing the output path, make sure that "StringCheck.xslt" is present
/// to make the XML transform work.
/// </remarks>
public const string OutputPath = @".";
/// <summary>
/// List of file extension to search.
/// </summary>
public static string[] TargetExtensions = new[] { "js", "html" };
/// <summary>
/// List of paths to exclude from search.
/// </summary>
public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" };
private TestContext testContextInstance;
/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}
[TestMethod]
public void ReportStringUsage()
{
this.CheckDashboardStrings(false);
}
[TestMethod]
public void ReportUnusedStrings()
{
this.CheckDashboardStrings(true);
}
private void CheckDashboardStrings(Boolean unusedOnly)
{
// Init Folders
var currentDir = System.IO.Directory.GetCurrentDirectory();
Trace("CurrentDir: {0}", currentDir);
var rootFolderInfo = ResolveFolder(currentDir, WebFolder);
Trace("Web Root: {0}", rootFolderInfo.FullName);
var outputFolderInfo = ResolveFolder(currentDir, OutputPath);
Trace("Output Path: {0}", outputFolderInfo.FullName);
// Load Strings
var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile);
if (!File.Exists(stringsFileName))
{
throw new Exception(string.Format("Strings file not found: {0}", stringsFileName));
}
int lineNumbers;
var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers);
Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers);
var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories);
var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => f.Name.EndsWith(e)));
var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p)));
var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList();
var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName);
Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count);
var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now));
var settings = new XmlWriterSettings
{
Indent = true,
Encoding = Encoding.UTF8,
WriteEndDocumentOnClose = true
};
Trace("Output file: {0}", outputFileName);
using (XmlWriter writer = XmlWriter.Create(outputFileName, settings))
{
writer.WriteStartDocument(true);
// Write the Processing Instruction node.
string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\"";
writer.WriteProcessingInstruction("xml-stylesheet", xslText);
writer.WriteStartElement("StringUsages");
writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report");
writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All");
foreach (var kvp in stringsDic)
{
var occurences = wordIndex.Find(kvp.Key);
if (occurences == null || !unusedOnly)
{
////Trace("{0}: {1}", kvp.Key, kvp.Value);
writer.WriteStartElement("Dictionary");
writer.WriteAttributeString("Token", kvp.Key);
writer.WriteAttributeString("Text", kvp.Value);
if (occurences != null && !unusedOnly)
{
foreach (var occurence in occurences)
{
writer.WriteStartElement("Occurence");
writer.WriteAttributeString("FileName", occurence.FileName);
writer.WriteAttributeString("FullPath", occurence.FullPath);
writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString());
writer.WriteEndElement();
////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber);
}
}
writer.WriteEndElement();
}
}
}
TestContext.AddResultFile(outputFileName);
TestContext.AddResultFile(outputFolderInfo.FullName);
}
private SortedDictionary<string, string> CreateStringsDictionary(FileInfo file, out int lineNumbers)
{
var dic = new SortedDictionary<string, string>();
lineNumbers = 0;
using (var reader = file.OpenText())
{
while (!reader.EndOfStream)
{
lineNumbers++;
var words = reader
.ReadLine()
.Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries);
if (words.Length == 2)
{
var token = words[0].Replace("\"", string.Empty).Trim();
var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim();
if (dic.Keys.Contains(token))
{
throw new Exception(string.Format("Double string entry found: {0}", token));
}
dic.Add(token, text);
}
}
}
return dic;
}
private DirectoryInfo ResolveFolder(string currentDir, string folderPath)
{
if (folderPath.IndexOf(@"\:") != 1)
{
folderPath = Path.Combine(currentDir, folderPath);
}
var folderInfo = new DirectoryInfo(folderPath);
if (!folderInfo.Exists)
{
throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName));
}
return folderInfo;
}
private void Trace(string message, params object[] parameters)
{
var formatted = string.Format(message, parameters);
System.Diagnostics.Trace.WriteLine(formatted);
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
{
public class IndexBuilder
{
public const int MinumumWordLength = 4;
public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
public static WordIndex BuildIndexFromFiles(IEnumerable<FileInfo> wordFiles, string rootFolderPath)
{
var index = new WordIndex();
var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray();
wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab
foreach (var file in wordFiles)
{
var lineNumber = 1;
var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty);
using (var reader = file.OpenText())
{
while (!reader.EndOfStream)
{
var words = reader
.ReadLine()
.Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries);
////.Select(f => f.Trim());
var wordIndex = 1;
foreach (var word in words)
{
if (word.Length >= MinumumWordLength)
{
index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++);
}
}
lineNumber++;
}
}
}
return index;
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
{
public class WordIndex : Dictionary<string, WordOccurrences>
{
public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase)
{
}
public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex)
{
WordOccurrences current;
if (!this.TryGetValue(word, out current))
{
current = new WordOccurrences();
this[word] = current;
}
current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex);
}
public WordOccurrences Find(string word)
{
WordOccurrences found;
if (this.TryGetValue(word, out found))
{
return found;
}
return null;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
{
public struct WordOccurrence
{
public readonly string FileName; // file containing the word.
public readonly string FullPath; // file containing the word.
public readonly int LineNumber; // line within the file.
public readonly int WordIndex; // index within the line.
public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex)
{
FileName = fileName;
FullPath = fullPath;
LineNumber = lineNumber;
WordIndex = wordIndex;
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing
{
public class WordOccurrences : List<WordOccurrence>
{
public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex)
{
this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex));
}
}
}

View File

@ -25,6 +25,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Debug\MediaBrowser.Tests.XML</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType> <DebugType>none</DebugType>
@ -36,6 +37,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.XML" />
</ItemGroup> </ItemGroup>
<Choose> <Choose>
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'"> <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
@ -50,6 +52,11 @@
</Otherwise> </Otherwise>
</Choose> </Choose>
<ItemGroup> <ItemGroup>
<Compile Include="ConsistencyTests\StringUsageReporter.cs" />
<Compile Include="ConsistencyTests\TextIndexing\IndexBuilder.cs" />
<Compile Include="ConsistencyTests\TextIndexing\WordIndex.cs" />
<Compile Include="ConsistencyTests\TextIndexing\WordOccurrence.cs" />
<Compile Include="ConsistencyTests\TextIndexing\WordOccurrences.cs" />
<Compile Include="MediaEncoding\Subtitles\AssParserTests.cs" /> <Compile Include="MediaEncoding\Subtitles\AssParserTests.cs" />
<Compile Include="MediaEncoding\Subtitles\SrtParserTests.cs" /> <Compile Include="MediaEncoding\Subtitles\SrtParserTests.cs" />
<Compile Include="MediaEncoding\Subtitles\VttWriterTest.cs" /> <Compile Include="MediaEncoding\Subtitles\VttWriterTest.cs" />
@ -98,6 +105,14 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ContentWithTargetPath Include="ConsistencyTests\Resources\StringCheck.xslt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<TargetPath>StringCheck.xslt</TargetPath>
</ContentWithTargetPath>
<None Include="ConsistencyTests\Resources\SampleTransformed.htm" />
<None Include="ConsistencyTests\Resources\StringCheckSample.xml" />
</ItemGroup>
<Choose> <Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup> <ItemGroup>

View File

@ -440,15 +440,7 @@ namespace MediaBrowser.WebDashboard.Api
files.Insert(0, "cordova.js"); files.Insert(0, "cordova.js");
} }
var tags = files.Select(s => var tags = files.Select(s => string.Format("<script src=\"{0}\" defer></script>", s)).ToArray();
{
if (s.IndexOf("require", StringComparison.OrdinalIgnoreCase) == -1 && s.IndexOf("alameda", StringComparison.OrdinalIgnoreCase) == -1)
{
return string.Format("<script src=\"{0}\" async></script>", s);
}
return string.Format("<script src=\"{0}\"></script>", s);
}).ToArray();
builder.Append(string.Join(string.Empty, tags)); builder.Append(string.Join(string.Empty, tags));

View File

@ -113,6 +113,9 @@
<Content Include="dashboard-ui\components\chromecasthelpers.js"> <Content Include="dashboard-ui\components\chromecasthelpers.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\components\directorybrowser\directorybrowser.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\favoriteitems.js"> <Content Include="dashboard-ui\components\favoriteitems.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -140,12 +143,6 @@
<Content Include="dashboard-ui\components\guestinviter\guestinviter.template.html"> <Content Include="dashboard-ui\components\guestinviter\guestinviter.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\components\metadataeditor\metadataeditor.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\metadataeditor\personeditor.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\navdrawer\navdrawer.css"> <Content Include="dashboard-ui\components\navdrawer\navdrawer.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -176,9 +173,6 @@
<Content Include="dashboard-ui\css\images\logo.png"> <Content Include="dashboard-ui\css\images\logo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\devices\windowsphone\wp.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\home.html"> <Content Include="dashboard-ui\home.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -230,12 +224,6 @@
<Content Include="dashboard-ui\components\medialibraryeditor\medialibraryeditor.template.html"> <Content Include="dashboard-ui\components\medialibraryeditor\medialibraryeditor.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\components\metadataeditor\personeditor.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\metadataeditor\metadataeditor.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\css\images\ani_equalizer_black.gif"> <Content Include="dashboard-ui\css\images\ani_equalizer_black.gif">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -1422,11 +1410,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="dashboard-ui\scripts\extensions.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="dashboard-ui\css\images\supporter\supporterbadge.png"> <Content Include="dashboard-ui\css\images\supporter\supporterbadge.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -1512,9 +1495,6 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="dashboard-ui\css\fonts\Montserrat.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="dashboard-ui\strings\ar.json"> <Content Include="dashboard-ui\strings\ar.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>

View File

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

View File

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

View File

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