made library scan a bit more conservative
This commit is contained in:
parent
a4cac9c95d
commit
30d6e2cd6c
|
@ -542,6 +542,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
var options = new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = 50
|
||||
};
|
||||
|
||||
Parallel.ForEach(nonCachedChildren, options, child =>
|
||||
|
@ -606,6 +607,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
_children.Add(item);
|
||||
}
|
||||
|
||||
if (saveTasks.Count > 50)
|
||||
{
|
||||
await Task.WhenAll(saveTasks).ConfigureAwait(false);
|
||||
saveTasks.Clear();
|
||||
}
|
||||
|
||||
saveTasks.Add(LibraryManager.SaveItem(item, CancellationToken.None));
|
||||
}
|
||||
|
||||
|
@ -642,65 +649,77 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
|
||||
/// <returns>Task.</returns>
|
||||
private Task RefreshChildren(IEnumerable<Tuple<BaseItem, bool>> children, IProgress<double> progress, CancellationToken cancellationToken, bool? recursive)
|
||||
private async Task RefreshChildren(IEnumerable<Tuple<BaseItem, bool>> children, IProgress<double> progress, CancellationToken cancellationToken, bool? recursive)
|
||||
{
|
||||
var list = children.ToList();
|
||||
|
||||
var percentages = new ConcurrentDictionary<Guid, double>(list.Select(i => new KeyValuePair<Guid, double>(i.Item1.Id, 0)));
|
||||
|
||||
var tasks = list.Select(tuple => Task.Run(async () =>
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var tuple in list)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var child = tuple.Item1;
|
||||
|
||||
//refresh it
|
||||
await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false);
|
||||
|
||||
// Refresh children if a folder and the item changed or recursive is set to true
|
||||
var refreshChildren = child.IsFolder && (tuple.Item2 || (recursive.HasValue && recursive.Value));
|
||||
|
||||
if (refreshChildren)
|
||||
if (tasks.Count > 50)
|
||||
{
|
||||
// Don't refresh children if explicitly set to false
|
||||
if (recursive.HasValue && recursive.Value == false)
|
||||
{
|
||||
refreshChildren = false;
|
||||
}
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (refreshChildren)
|
||||
Tuple<BaseItem, bool> currentTuple = tuple;
|
||||
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
var child = currentTuple.Item1;
|
||||
|
||||
innerProgress.RegisterAction(p =>
|
||||
//refresh it
|
||||
await child.RefreshMetadata(cancellationToken, resetResolveArgs: child.IsFolder).ConfigureAwait(false);
|
||||
|
||||
// Refresh children if a folder and the item changed or recursive is set to true
|
||||
var refreshChildren = child.IsFolder && (currentTuple.Item2 || (recursive.HasValue && recursive.Value));
|
||||
|
||||
if (refreshChildren)
|
||||
{
|
||||
percentages.TryUpdate(child.Id, p / 100, percentages[child.Id]);
|
||||
// Don't refresh children if explicitly set to false
|
||||
if (recursive.HasValue && recursive.Value == false)
|
||||
{
|
||||
refreshChildren = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (refreshChildren)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
innerProgress.RegisterAction(p =>
|
||||
{
|
||||
percentages.TryUpdate(child.Id, p / 100, percentages[child.Id]);
|
||||
|
||||
var percent = percentages.Values.Sum();
|
||||
percent /= list.Count;
|
||||
|
||||
progress.Report((90 * percent) + 10);
|
||||
});
|
||||
|
||||
await ((Folder)child).ValidateChildren(innerProgress, cancellationToken, recursive).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
percentages.TryUpdate(child.Id, 1, percentages[child.Id]);
|
||||
|
||||
var percent = percentages.Values.Sum();
|
||||
percent /= list.Count;
|
||||
|
||||
progress.Report((90 * percent) + 10);
|
||||
});
|
||||
|
||||
await ((Folder) child).ValidateChildren(innerProgress, cancellationToken, recursive).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
percentages.TryUpdate(child.Id, 1, percentages[child.Id]);
|
||||
|
||||
var percent = percentages.Values.Sum();
|
||||
percent /= list.Count;
|
||||
|
||||
progress.Report((90 * percent) + 10);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
<Compile Include="Providers\IImageEnhancer.cs" />
|
||||
<Compile Include="Providers\ImagesByNameProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BaseFFMpegProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFMpegAudioImageProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\AudioImageProvider.cs" />
|
||||
<Compile Include="Providers\MediaInfo\BaseFFProbeProvider.cs" />
|
||||
<Compile Include="Providers\BaseProviderInfo.cs" />
|
||||
<Compile Include="Providers\Movies\FanArtMovieProvider.cs" />
|
||||
|
@ -170,7 +170,6 @@
|
|||
<Compile Include="Providers\TV\RemoteSeriesProvider.cs" />
|
||||
<Compile Include="Providers\TV\SeriesProviderFromXml.cs" />
|
||||
<Compile Include="Providers\TV\SeriesXmlParser.cs" />
|
||||
<Compile Include="Providers\MediaInfo\FFMpegVideoImageProvider.cs" />
|
||||
<Compile Include="Resolvers\IResolverIgnoreRule.cs" />
|
||||
<Compile Include="Resolvers\EntityResolutionHelper.cs" />
|
||||
<Compile Include="Resolvers\ResolverPriority.cs" />
|
||||
|
|
|
@ -23,12 +23,6 @@ namespace MediaBrowser.Controller.MediaInfo
|
|||
/// <value>The video image cache.</value>
|
||||
internal FileSystemRepository VideoImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image cache.
|
||||
/// </summary>
|
||||
/// <value>The image cache.</value>
|
||||
internal FileSystemRepository AudioImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle cache.
|
||||
/// </summary>
|
||||
|
@ -54,7 +48,6 @@ namespace MediaBrowser.Controller.MediaInfo
|
|||
_libraryManager = libraryManager;
|
||||
|
||||
VideoImageCache = new FileSystemRepository(VideoImagesDataPath);
|
||||
AudioImageCache = new FileSystemRepository(AudioImagesDataPath);
|
||||
SubtitleCache = new FileSystemRepository(SubtitleCachePath);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses ffmpeg to create video images
|
||||
/// </summary>
|
||||
public class AudioImageProvider : BaseMetadataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logManager">The log manager.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
public AudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
|
||||
: base(logManager, configurationManager)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The true task result
|
||||
/// </summary>
|
||||
protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
return item.LocationType == LocationType.FileSystem && item is Audio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// to determine if this provider should be re-fetched.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>DateTime.</returns>
|
||||
protected override DateTime CompareDate(BaseItem item)
|
||||
{
|
||||
return item.DateModified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Last; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.NeedsRefreshInternal(item, providerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
public override Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
if (force || string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
var album = item.ResolveArgs.Parent as MusicAlbum;
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
// First try to use the parent's image
|
||||
item.PrimaryImagePath = item.ResolveArgs.Parent.PrimaryImagePath;
|
||||
}
|
||||
}
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return TrueTaskResult;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
using MediaBrowser.Common.MediaInfo;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses ffmpeg to create video images
|
||||
/// </summary>
|
||||
public class FFMpegAudioImageProvider : BaseFFMpegProvider<Audio>
|
||||
{
|
||||
public FFMpegAudioImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder)
|
||||
: base(logManager, configurationManager, mediaEncoder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The true task result
|
||||
/// </summary>
|
||||
protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Last; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _locks
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private SemaphoreSlim GetLock(string filename)
|
||||
{
|
||||
return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.NeedsRefreshInternal(item, providerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
public override async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = ProviderRefreshStatus.Success;
|
||||
|
||||
if (force || string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
var album = item.ResolveArgs.Parent as MusicAlbum;
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
// First try to use the parent's image
|
||||
item.PrimaryImagePath = item.ResolveArgs.Parent.PrimaryImagePath;
|
||||
}
|
||||
|
||||
// If it's still empty see if there's an embedded image
|
||||
if (force || string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
var audio = (Audio)item;
|
||||
|
||||
if (audio.MediaStreams != null && audio.MediaStreams.Any(s => s.Type == MediaStreamType.Video))
|
||||
{
|
||||
var filename = album != null && string.IsNullOrEmpty(audio.Album + album.DateModified.Ticks) ? (audio.Id.ToString() + audio.DateModified.Ticks) : audio.Album;
|
||||
|
||||
var path = Kernel.Instance.FFMpegManager.AudioImageCache.GetResourcePath(filename + "_primary", ".jpg");
|
||||
|
||||
if (!Kernel.Instance.FFMpegManager.AudioImageCache.ContainsFilePath(path))
|
||||
{
|
||||
var semaphore = GetLock(path);
|
||||
|
||||
// Acquire a lock
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check again
|
||||
if (!Kernel.Instance.FFMpegManager.AudioImageCache.ContainsFilePath(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
await MediaEncoder.ExtractImage(new[] { audio.Path }, InputType.AudioFile, null, path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = ProviderRefreshStatus.Failure;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
if (success == ProviderRefreshStatus.Success)
|
||||
{
|
||||
// Image is already in the cache
|
||||
audio.PrimaryImagePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow, success);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.MediaInfo;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers.MediaInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses ffmpeg to create video images
|
||||
/// </summary>
|
||||
public class FfMpegVideoImageProvider : BaseFFMpegProvider<Video>
|
||||
{
|
||||
/// <summary>
|
||||
/// The _iso manager
|
||||
/// </summary>
|
||||
private readonly IIsoManager _isoManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FfMpegVideoImageProvider" /> class.
|
||||
/// </summary>
|
||||
/// <param name="isoManager">The iso manager.</param>
|
||||
/// <param name="logManager">The log manager.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
public FfMpegVideoImageProvider(IIsoManager isoManager, ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder)
|
||||
: base(logManager, configurationManager, mediaEncoder)
|
||||
{
|
||||
_isoManager = isoManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Last; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supportses the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
public override bool Supports(BaseItem item)
|
||||
{
|
||||
if (item.LocationType != LocationType.FileSystem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var video = item as Video;
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
if (video.VideoType == VideoType.Iso && _isoManager.CanMount(item.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can only extract images from folder rips if we know the largest stream path
|
||||
return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Needses the refresh internal.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="providerInfo">The provider info.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.NeedsRefreshInternal(item, providerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The true task result
|
||||
/// </summary>
|
||||
protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="force">if set to <c>true</c> [force].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
public override Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
if (force || string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
var video = (Video)item;
|
||||
|
||||
// We can only extract images from videos if we know there's an embedded video stream
|
||||
if (video.MediaStreams != null && video.MediaStreams.Any(m => m.Type == MediaStreamType.Video))
|
||||
{
|
||||
var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
|
||||
|
||||
var path = Kernel.Instance.FFMpegManager.VideoImageCache.GetResourcePath(filename, ".jpg");
|
||||
|
||||
if (!Kernel.Instance.FFMpegManager.VideoImageCache.ContainsFilePath(path))
|
||||
{
|
||||
return ExtractImage(video, path, cancellationToken);
|
||||
}
|
||||
|
||||
// Image is already in the cache
|
||||
item.PrimaryImagePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
SetLastRefreshed(item, DateTime.UtcNow);
|
||||
return TrueTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the iso if needed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IsoMount.</returns>
|
||||
protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item.VideoType == VideoType.Iso)
|
||||
{
|
||||
return _isoManager.Mount(item.Path, cancellationToken);
|
||||
}
|
||||
|
||||
return NullMountTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the image.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{System.Boolean}.</returns>
|
||||
private async Task<bool> ExtractImage(Video video, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
|
||||
// Always use 10 seconds for dvd because our duration could be out of whack
|
||||
var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue &&
|
||||
video.RunTimeTicks.Value > 0
|
||||
? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
|
||||
: TimeSpan.FromSeconds(10);
|
||||
|
||||
InputType type;
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
|
||||
|
||||
await MediaEncoder.ExtractImage(inputPath, type, imageOffset, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
video.PrimaryImagePath = path;
|
||||
SetLastRefreshed(video, DateTime.UtcNow);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetLastRefreshed(video, DateTime.UtcNow, ProviderRefreshStatus.Failure);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,20 @@ namespace MediaBrowser.Model.Entities
|
|||
/// </summary>
|
||||
public interface IHasMediaStreams
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the media streams.
|
||||
/// </summary>
|
||||
/// <value>The media streams.</value>
|
||||
List<MediaStream> MediaStreams { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
string Path { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the primary image path.
|
||||
/// </summary>
|
||||
/// <value>The primary image path.</value>
|
||||
string PrimaryImagePath { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,7 +153,9 @@
|
|||
<Compile Include="ScheduledTasks\ChapterImagesTask.cs" />
|
||||
<Compile Include="ScheduledTasks\ImageCleanupTask.cs" />
|
||||
<Compile Include="ScheduledTasks\PluginUpdateTask.cs" />
|
||||
<Compile Include="ScheduledTasks\AudioImagesTask.cs" />
|
||||
<Compile Include="ScheduledTasks\RefreshMediaLibraryTask.cs" />
|
||||
<Compile Include="ScheduledTasks\VideoImagesTask.cs" />
|
||||
<Compile Include="ServerApplicationPaths.cs" />
|
||||
<Compile Include="ServerManager\ServerManager.cs" />
|
||||
<Compile Include="ServerManager\WebSocketConnection.cs" />
|
||||
|
|
|
@ -50,12 +50,12 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|||
/// <summary>
|
||||
/// The video image resource pool
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _videoImageResourcePool = new SemaphoreSlim(2, 2);
|
||||
private readonly SemaphoreSlim _videoImageResourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The audio image resource pool
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _audioImageResourcePool = new SemaphoreSlim(3, 3);
|
||||
private readonly SemaphoreSlim _audioImageResourcePool = new SemaphoreSlim(2, 2);
|
||||
|
||||
/// <summary>
|
||||
/// The _subtitle extraction resource pool
|
||||
|
@ -65,7 +65,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|||
/// <summary>
|
||||
/// The FF probe resource pool
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(3, 3);
|
||||
private readonly SemaphoreSlim _ffProbeResourcePool = new SemaphoreSlim(2, 2);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the versioned directory path.
|
||||
|
@ -370,7 +370,18 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ffProbeResourcePool.Release();
|
||||
|
||||
_logger.ErrorException("Error starting ffprobe", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Task<string> standardErrorReadTask = null;
|
||||
|
||||
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.MediaInfo;
|
||||
using MediaBrowser.Common.ScheduledTasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Class AudioImagesTask
|
||||
/// </summary>
|
||||
public class AudioImagesTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the image cache.
|
||||
/// </summary>
|
||||
/// <value>The image cache.</value>
|
||||
public FileSystemRepository ImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _library manager
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
/// <summary>
|
||||
/// The _media encoder
|
||||
/// </summary>
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
/// <summary>
|
||||
/// The _locks
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioImagesTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
public AudioImagesTask(ILibraryManager libraryManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
|
||||
ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.AudioImagesDataPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the task
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get { return "Audio image extraction"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description.
|
||||
/// </summary>
|
||||
/// <value>The description.</value>
|
||||
public string Description
|
||||
{
|
||||
get { return "Extracts images from audio files that do not have external images."; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the category.
|
||||
/// </summary>
|
||||
/// <value>The category.</value>
|
||||
public string Category
|
||||
{
|
||||
get { return "Library"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the task
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var items = _libraryManager.RootFolder.RecursiveChildren
|
||||
.OfType<Audio>()
|
||||
.Where(i => i.LocationType == LocationType.FileSystem && string.IsNullOrEmpty(i.PrimaryImagePath) && i.MediaStreams != null && i.MediaStreams.Any(m => m.Type == MediaStreamType.Video))
|
||||
.ToList();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var album = item.Parent as MusicAlbum;
|
||||
|
||||
var filename = album != null && string.IsNullOrEmpty(item.Album + album.DateModified.Ticks) ? (item.Id.ToString() + item.DateModified.Ticks) : item.Album;
|
||||
|
||||
var path = ImageCache.GetResourcePath(filename + "_primary", ".jpg");
|
||||
|
||||
var success = true;
|
||||
|
||||
if (!ImageCache.ContainsFilePath(path))
|
||||
{
|
||||
var semaphore = GetLock(path);
|
||||
|
||||
// Acquire a lock
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check again
|
||||
if (!ImageCache.ContainsFilePath(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediaEncoder.ExtractImage(new[] { item.Path }, InputType.AudioFile, null, path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= items.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
|
||||
if (success)
|
||||
{
|
||||
// Image is already in the cache
|
||||
item.PrimaryImagePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default triggers.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
|
||||
public IEnumerable<ITaskTrigger> GetDefaultTriggers()
|
||||
{
|
||||
return new ITaskTrigger[]
|
||||
{
|
||||
new DailyTrigger { TimeOfDay = TimeSpan.FromHours(1) }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private SemaphoreSlim GetLock(string filename)
|
||||
{
|
||||
return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,9 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
|||
/// The _logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// The _library manager
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
|
@ -99,7 +102,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
|||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get { return "Create video chapter thumbnails"; }
|
||||
get { return "Chapter image extraction"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.MediaInfo;
|
||||
using MediaBrowser.Common.ScheduledTasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers.MediaInfo;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.ScheduledTasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Class VideoImagesTask
|
||||
/// </summary>
|
||||
public class VideoImagesTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the image cache.
|
||||
/// </summary>
|
||||
/// <value>The image cache.</value>
|
||||
public FileSystemRepository ImageCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _library manager
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
/// <summary>
|
||||
/// The _media encoder
|
||||
/// </summary>
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
/// <summary>
|
||||
/// The _iso manager
|
||||
/// </summary>
|
||||
private readonly IIsoManager _isoManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _locks
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioImagesTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="isoManager">The iso manager.</param>
|
||||
public VideoImagesTask(ILibraryManager libraryManager, IMediaEncoder mediaEncoder, IIsoManager isoManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_isoManager = isoManager;
|
||||
|
||||
ImageCache = new FileSystemRepository(Kernel.Instance.FFMpegManager.VideoImagesDataPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the task
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name
|
||||
{
|
||||
get { return "Video image extraction"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description.
|
||||
/// </summary>
|
||||
/// <value>The description.</value>
|
||||
public string Description
|
||||
{
|
||||
get { return "Extracts images from audio files that do not have external images."; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the category.
|
||||
/// </summary>
|
||||
/// <value>The category.</value>
|
||||
public string Category
|
||||
{
|
||||
get { return "Library"; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the task
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
var items = _libraryManager.RootFolder.RecursiveChildren
|
||||
.OfType<Video>()
|
||||
.Where(i =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(i.PrimaryImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i.LocationType != LocationType.FileSystem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i.VideoType == VideoType.HdDvd)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i.VideoType == VideoType.Iso && !i.IsoType.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return i.MediaStreams != null && i.MediaStreams.Any(m => m.Type == MediaStreamType.Video);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
|
||||
|
||||
var path = ImageCache.GetResourcePath(filename, ".jpg");
|
||||
|
||||
var success = true;
|
||||
|
||||
if (!ImageCache.ContainsFilePath(path))
|
||||
{
|
||||
var semaphore = GetLock(path);
|
||||
|
||||
// Acquire a lock
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check again
|
||||
if (!ImageCache.ContainsFilePath(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExtractImage(item, path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
success = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= items.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
|
||||
if (success)
|
||||
{
|
||||
// Image is already in the cache
|
||||
item.PrimaryImagePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the image.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task ExtractImage(Video video, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
|
||||
// Always use 10 seconds for dvd because our duration could be out of whack
|
||||
var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue &&
|
||||
video.RunTimeTicks.Value > 0
|
||||
? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
|
||||
: TimeSpan.FromSeconds(10);
|
||||
|
||||
InputType type;
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
|
||||
|
||||
await _mediaEncoder.ExtractImage(inputPath, type, imageOffset, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
video.PrimaryImagePath = path;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The null mount task result
|
||||
/// </summary>
|
||||
protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
|
||||
|
||||
/// <summary>
|
||||
/// Mounts the iso if needed.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IIsoMount}.</returns>
|
||||
protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item.VideoType == VideoType.Iso)
|
||||
{
|
||||
return _isoManager.Mount(item.Path, cancellationToken);
|
||||
}
|
||||
|
||||
return NullMountTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default triggers.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
|
||||
public IEnumerable<ITaskTrigger> GetDefaultTriggers()
|
||||
{
|
||||
return new ITaskTrigger[]
|
||||
{
|
||||
new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private SemaphoreSlim GetLock(string filename)
|
||||
{
|
||||
return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
@ -159,7 +160,7 @@ namespace MediaBrowser.ServerApplication.Controls
|
|||
DisplayTitle(item);
|
||||
DisplayRating(item);
|
||||
|
||||
var path = MultiItemUpdateNotification.GetImagePath(item);
|
||||
var path = GetImagePath(item);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
|
@ -210,6 +211,44 @@ namespace MediaBrowser.ServerApplication.Controls
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
internal static string GetImagePath(BaseItem item)
|
||||
{
|
||||
// Try our best to find an image
|
||||
var path = item.PrimaryImagePath;
|
||||
|
||||
if (string.IsNullOrEmpty(path) && item.BackdropImagePaths != null)
|
||||
{
|
||||
path = item.BackdropImagePaths.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Art);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Logo);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Disc);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays the rating.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<UserControl x:Class="MediaBrowser.ServerApplication.Controls.MultiItemUpdateNotification"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300">
|
||||
|
||||
<Grid MaxHeight="400" MaxWidth="600" Margin="20">
|
||||
<Border BorderThickness="0" Background="#333333">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="25" ShadowDepth="0">
|
||||
|
||||
</DropShadowEffect>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Grid>
|
||||
<Grid.Background>
|
||||
<LinearGradientBrush SpreadMethod="Reflect" ColorInterpolationMode="SRgbLinearInterpolation" StartPoint="0,0" EndPoint="0,1" >
|
||||
<GradientStop Color="#ff222222" Offset="0" />
|
||||
<GradientStop Color="#ffbbbbbb" Offset="1" />
|
||||
</LinearGradientBrush>
|
||||
</Grid.Background>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock x:Name="header" FontSize="26" Foreground="White" Grid.Row="0"></TextBlock>
|
||||
|
||||
<UniformGrid x:Name="itemsPanel" Columns="4" Margin="0 20 0 0" Grid.Row="1"></UniformGrid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
|
@ -1,151 +0,0 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace MediaBrowser.ServerApplication.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MultiItemUpdateNotification.xaml
|
||||
/// </summary>
|
||||
public partial class MultiItemUpdateNotification : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
private readonly ILogger Logger;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children changed event args.
|
||||
/// </summary>
|
||||
/// <value>The children changed event args.</value>
|
||||
private List<BaseItem> Items
|
||||
{
|
||||
get { return DataContext as List<BaseItem>; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiItemUpdateNotification" /> class.
|
||||
/// </summary>
|
||||
public MultiItemUpdateNotification(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
Logger = logger;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Loaded += MultiItemUpdateNotification_Loaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Loaded event of the MultiItemUpdateNotification control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="RoutedEventArgs" /> instance containing the event data.</param>
|
||||
void MultiItemUpdateNotification_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
header.Text = string.Format("{0} New Items!", Items.Count);
|
||||
|
||||
PopulateItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the items.
|
||||
/// </summary>
|
||||
private void PopulateItems()
|
||||
{
|
||||
itemsPanel.Children.Clear();
|
||||
|
||||
var items = Items;
|
||||
|
||||
const int maxItemsToDisplay = 8;
|
||||
var index = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (index >= maxItemsToDisplay)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Try our best to find an image
|
||||
var path = GetImagePath(item);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Image img;
|
||||
|
||||
try
|
||||
{
|
||||
img = App.Instance.GetImage(path);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
Logger.Error("Image file not found {0}", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
img.Stretch = Stretch.Uniform;
|
||||
img.Margin = new Thickness(0, 0, 5, 5);
|
||||
img.ToolTip = ItemUpdateNotification.GetDisplayName(item, true);
|
||||
RenderOptions.SetBitmapScalingMode(img, BitmapScalingMode.Fant);
|
||||
itemsPanel.Children.Add(img);
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
internal static string GetImagePath(BaseItem item)
|
||||
{
|
||||
// Try our best to find an image
|
||||
var path = item.PrimaryImagePath;
|
||||
|
||||
if (string.IsNullOrEmpty(path) && item.BackdropImagePaths != null)
|
||||
{
|
||||
path = item.BackdropImagePaths.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Art);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Logo);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
path = item.GetImage(ImageType.Disc);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,7 +109,7 @@ namespace MediaBrowser.ServerApplication.EntryPoints
|
|||
}
|
||||
|
||||
// Show the notification
|
||||
if (newItems.Count == 1)
|
||||
if (newItems.Count > 0)
|
||||
{
|
||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
|
@ -122,19 +122,6 @@ namespace MediaBrowser.ServerApplication.EntryPoints
|
|||
}, PopupAnimation.Slide, 6000));
|
||||
});
|
||||
}
|
||||
else if (newItems.Count > 1)
|
||||
{
|
||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var window = (MainWindow)Application.Current.MainWindow;
|
||||
|
||||
window.Dispatcher.InvokeAsync(() => window.MbTaskbarIcon.ShowCustomBalloon(new MultiItemUpdateNotification(_logger)
|
||||
{
|
||||
DataContext = newItems
|
||||
|
||||
}, PopupAnimation.Slide, 6000));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -194,10 +194,6 @@
|
|||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\MultiItemUpdateNotification.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="LibraryExplorer.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
@ -221,9 +217,6 @@
|
|||
<Compile Include="Controls\ItemUpdateNotification.xaml.cs">
|
||||
<DependentUpon>ItemUpdateNotification.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\MultiItemUpdateNotification.xaml.cs">
|
||||
<DependentUpon>MultiItemUpdateNotification.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Implementations\DotNetZipClient.cs" />
|
||||
<Compile Include="LibraryExplorer.xaml.cs">
|
||||
<DependentUpon>LibraryExplorer.xaml</DependentUpon>
|
||||
|
|
Loading…
Reference in New Issue
Block a user