From 868a7ce9c8b50bd64c8b5ae33fec77abfd25ef7c Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Thu, 21 Feb 2013 23:23:06 -0500 Subject: isolated clickonce dependancies --- .../Entities/CollectionFolder.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 18 +- MediaBrowser.Controller/Entities/User.cs | 6 +- MediaBrowser.Controller/IO/NetworkShares.cs | 644 +++++++++++++++++++++ MediaBrowser.Controller/Library/LibraryManager.cs | 14 +- .../MediaBrowser.Controller.csproj | 2 +- .../ScheduledTasks/ChapterImagesTask.cs | 4 +- .../ScheduledTasks/ImageCleanupTask.cs | 4 +- .../ScheduledTasks/PeopleValidationTask.cs | 2 +- .../ScheduledTasks/PluginUpdateTask.cs | 10 +- .../ScheduledTasks/RefreshMediaLibraryTask.cs | 4 +- 11 files changed, 677 insertions(+), 33 deletions(-) create mode 100644 MediaBrowser.Controller/IO/NetworkShares.cs (limited to 'MediaBrowser.Controller') diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 33e9f291d..fbcd2f589 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [recursive]. /// Task. - protected override Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) + protected override Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) { //we don't directly validate our children //but we do need to clear out the index cache... diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index f67934f5d..bcb0b26be 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -612,7 +612,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [recursive]. /// Task. - public async Task ValidateChildren(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) + public async Task ValidateChildren(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) { cancellationToken.ThrowIfCancellationRequested(); @@ -664,7 +664,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [recursive]. /// Task. - protected async virtual Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) + protected async virtual Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool? recursive = null) { // Nothing to do here if (LocationType != LocationType.FileSystem) @@ -681,7 +681,7 @@ namespace MediaBrowser.Controller.Entities if (nonCachedChildren == null) return; //nothing to validate - progress.Report(new TaskProgress { PercentComplete = 5 }); + progress.Report(5); //build a dictionary of the current children we have now by Id so we can compare quickly and easily var currentChildren = ActualChildren.ToDictionary(i => i.Id); @@ -772,13 +772,13 @@ namespace MediaBrowser.Controller.Entities Kernel.Instance.LibraryManager.OnLibraryChanged(changedArgs); } - progress.Report(new TaskProgress { PercentComplete = 15 }); + progress.Report(15); cancellationToken.ThrowIfCancellationRequested(); await RefreshChildren(validChildren, progress, cancellationToken, recursive).ConfigureAwait(false); - progress.Report(new TaskProgress { PercentComplete = 100 }); + progress.Report(100); } /// @@ -789,7 +789,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// if set to true [recursive]. /// Task. - private Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive) + private Task RefreshChildren(IEnumerable> children, IProgress progress, CancellationToken cancellationToken, bool? recursive) { var numComplete = 0; @@ -824,7 +824,7 @@ namespace MediaBrowser.Controller.Entities { cancellationToken.ThrowIfCancellationRequested(); - await ((Folder)child).ValidateChildren(new Progress { }, cancellationToken, recursive: recursive).ConfigureAwait(false); + await ((Folder)child).ValidateChildren(new Progress { }, cancellationToken, recursive: recursive).ConfigureAwait(false); } lock (progress) @@ -834,7 +834,7 @@ namespace MediaBrowser.Controller.Entities double percent = numComplete; percent /= list.Count; - progress.Report(new TaskProgress { PercentComplete = (85 * percent) + 15 }); + progress.Report((85 * percent) + 15); } })); @@ -952,7 +952,7 @@ namespace MediaBrowser.Controller.Entities { await base.ChangedExternally().ConfigureAwait(false); - var progress = new Progress { }; + var progress = new Progress { }; await ValidateChildren(progress, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index ee9815e62..f27e8c689 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -205,7 +205,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// The progress. /// Task. - public async Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) + public async Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) { Logger.Info("Validating media library for {0}", Name); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.Entities /// The cancellation token. /// The progress. /// Task. - public async Task ValidateCollectionFolders(IProgress progress, CancellationToken cancellationToken) + public async Task ValidateCollectionFolders(IProgress progress, CancellationToken cancellationToken) { Logger.Info("Validating collection folders for {0}", Name); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -279,7 +279,7 @@ namespace MediaBrowser.Controller.Entities RootFolder = null; // Kick off a task to validate the media library - Task.Run(() => ValidateMediaLibrary(new Progress { }, CancellationToken.None)); + Task.Run(() => ValidateMediaLibrary(new Progress { }, CancellationToken.None)); return RefreshMetadata(CancellationToken.None, forceSave: true, forceRefresh: true); } diff --git a/MediaBrowser.Controller/IO/NetworkShares.cs b/MediaBrowser.Controller/IO/NetworkShares.cs new file mode 100644 index 000000000..93edc6447 --- /dev/null +++ b/MediaBrowser.Controller/IO/NetworkShares.cs @@ -0,0 +1,644 @@ +using System; +using System.IO; +using System.Collections; +using System.Runtime.InteropServices; + +namespace MediaBrowser.Controller.IO +{ + /// + /// Type of share + /// + [Flags] + public enum ShareType + { + /// Disk share + Disk = 0, + /// Printer share + Printer = 1, + /// Device share + Device = 2, + /// IPC share + IPC = 3, + /// Special share + Special = -2147483648, // 0x80000000, + } + + #region Share + + /// + /// Information about a local share + /// + public class Share + { + #region Private data + + private string _server; + private string _netName; + private string _path; + private ShareType _shareType; + private string _remark; + + #endregion + + #region Constructor + + /// + /// Constructor + /// + /// + /// + public Share(string server, string netName, string path, ShareType shareType, string remark) + { + if (ShareType.Special == shareType && "IPC$" == netName) + { + shareType |= ShareType.IPC; + } + + _server = server; + _netName = netName; + _path = path; + _shareType = shareType; + _remark = remark; + } + + #endregion + + #region Properties + + /// + /// The name of the computer that this share belongs to + /// + public string Server + { + get { return _server; } + } + + /// + /// Share name + /// + public string NetName + { + get { return _netName; } + } + + /// + /// Local path + /// + public string Path + { + get { return _path; } + } + + /// + /// Share type + /// + public ShareType ShareType + { + get { return _shareType; } + } + + /// + /// Comment + /// + public string Remark + { + get { return _remark; } + } + + /// + /// Returns true if this is a file system share + /// + public bool IsFileSystem + { + get + { + // Shared device + if (0 != (_shareType & ShareType.Device)) return false; + // IPC share + if (0 != (_shareType & ShareType.IPC)) return false; + // Shared printer + if (0 != (_shareType & ShareType.Printer)) return false; + + // Standard disk share + if (0 == (_shareType & ShareType.Special)) return true; + + // Special disk share (e.g. C$) + if (ShareType.Special == _shareType && null != _netName && 0 != _netName.Length) + return true; + else + return false; + } + } + + /// + /// Get the root of a disk-based share + /// + public DirectoryInfo Root + { + get + { + if (IsFileSystem) + { + if (null == _server || 0 == _server.Length) + if (null == _path || 0 == _path.Length) + return new DirectoryInfo(ToString()); + else + return new DirectoryInfo(_path); + else + return new DirectoryInfo(ToString()); + } + else + return null; + } + } + + #endregion + + /// + /// Returns the path to this share + /// + /// + public override string ToString() + { + if (null == _server || 0 == _server.Length) + { + return string.Format(@"\\{0}\{1}", Environment.MachineName, _netName); + } + else + return string.Format(@"\\{0}\{1}", _server, _netName); + } + + /// + /// Returns true if this share matches the local path + /// + /// + /// + public bool MatchesPath(string path) + { + if (!IsFileSystem) return false; + if (null == path || 0 == path.Length) return true; + + return path.ToLower().StartsWith(_path.ToLower()); + } + } + + #endregion + + /// + /// A collection of shares + /// + public class ShareCollection : ReadOnlyCollectionBase + { + #region Platform + + /// + /// Is this an NT platform? + /// + protected static bool IsNT + { + get { return (PlatformID.Win32NT == Environment.OSVersion.Platform); } + } + + /// + /// Returns true if this is Windows 2000 or higher + /// + protected static bool IsW2KUp + { + get + { + OperatingSystem os = Environment.OSVersion; + if (PlatformID.Win32NT == os.Platform && os.Version.Major >= 5) + return true; + else + return false; + } + } + + #endregion + + #region Interop + + #region Constants + + /// Maximum path length + protected const int MAX_PATH = 260; + /// No error + protected const int NO_ERROR = 0; + /// Access denied + protected const int ERROR_ACCESS_DENIED = 5; + /// Access denied + protected const int ERROR_WRONG_LEVEL = 124; + /// More data available + protected const int ERROR_MORE_DATA = 234; + /// Not connected + protected const int ERROR_NOT_CONNECTED = 2250; + /// Level 1 + protected const int UNIVERSAL_NAME_INFO_LEVEL = 1; + /// Max extries (9x) + protected const int MAX_SI50_ENTRIES = 20; + + #endregion + + #region Structures + + /// Unc name + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + protected struct UNIVERSAL_NAME_INFO + { + [MarshalAs(UnmanagedType.LPTStr)] + public string lpUniversalName; + } + + /// Share information, NT, level 2 + /// + /// Requires admin rights to work. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + protected struct SHARE_INFO_2 + { + [MarshalAs(UnmanagedType.LPWStr)] + public string NetName; + public ShareType ShareType; + [MarshalAs(UnmanagedType.LPWStr)] + public string Remark; + public int Permissions; + public int MaxUsers; + public int CurrentUsers; + [MarshalAs(UnmanagedType.LPWStr)] + public string Path; + [MarshalAs(UnmanagedType.LPWStr)] + public string Password; + } + + /// Share information, NT, level 1 + /// + /// Fallback when no admin rights. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + protected struct SHARE_INFO_1 + { + [MarshalAs(UnmanagedType.LPWStr)] + public string NetName; + public ShareType ShareType; + [MarshalAs(UnmanagedType.LPWStr)] + public string Remark; + } + + /// Share information, Win9x + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] + protected struct SHARE_INFO_50 + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)] + public string NetName; + + public byte bShareType; + public ushort Flags; + + [MarshalAs(UnmanagedType.LPTStr)] + public string Remark; + [MarshalAs(UnmanagedType.LPTStr)] + public string Path; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)] + public string PasswordRW; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)] + public string PasswordRO; + + public ShareType ShareType + { + get { return (ShareType)((int)bShareType & 0x7F); } + } + } + + /// Share information level 1, Win9x + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] + protected struct SHARE_INFO_1_9x + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 13)] + public string NetName; + public byte Padding; + + public ushort bShareType; + + [MarshalAs(UnmanagedType.LPTStr)] + public string Remark; + + public ShareType ShareType + { + get { return (ShareType)((int)bShareType & 0x7FFF); } + } + } + + #endregion + + #region Functions + + /// Get a UNC name + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, ref UNIVERSAL_NAME_INFO lpBuffer, ref int lpBufferSize); + + /// Get a UNC name + [DllImport("mpr", CharSet = CharSet.Auto)] + protected static extern int WNetGetUniversalName(string lpLocalPath, + int dwInfoLevel, IntPtr lpBuffer, ref int lpBufferSize); + + /// Enumerate shares (NT) + [DllImport("netapi32", CharSet = CharSet.Unicode)] + protected static extern int NetShareEnum(string lpServerName, int dwLevel, + out IntPtr lpBuffer, int dwPrefMaxLen, out int entriesRead, + out int totalEntries, ref int hResume); + + /// Enumerate shares (9x) + [DllImport("svrapi", CharSet = CharSet.Ansi)] + protected static extern int NetShareEnum( + [MarshalAs(UnmanagedType.LPTStr)] string lpServerName, int dwLevel, + IntPtr lpBuffer, ushort cbBuffer, out ushort entriesRead, + out ushort totalEntries); + + /// Free the buffer (NT) + [DllImport("netapi32")] + protected static extern int NetApiBufferFree(IntPtr lpBuffer); + + #endregion + + #region Enumerate shares + + /// + /// Enumerates the shares on Windows NT + /// + /// The server name + /// The ShareCollection + protected static void EnumerateSharesNT(string server, ShareCollection shares) + { + int level = 2; + int entriesRead, totalEntries, nRet, hResume = 0; + IntPtr pBuffer = IntPtr.Zero; + + try + { + nRet = NetShareEnum(server, level, out pBuffer, -1, + out entriesRead, out totalEntries, ref hResume); + + if (ERROR_ACCESS_DENIED == nRet) + { + //Need admin for level 2, drop to level 1 + level = 1; + nRet = NetShareEnum(server, level, out pBuffer, -1, + out entriesRead, out totalEntries, ref hResume); + } + + if (NO_ERROR == nRet && entriesRead > 0) + { + Type t = (2 == level) ? typeof(SHARE_INFO_2) : typeof(SHARE_INFO_1); + int offset = Marshal.SizeOf(t); + + for (int i = 0, lpItem = pBuffer.ToInt32(); i < entriesRead; i++, lpItem += offset) + { + IntPtr pItem = new IntPtr(lpItem); + if (1 == level) + { + SHARE_INFO_1 si = (SHARE_INFO_1)Marshal.PtrToStructure(pItem, t); + shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark); + } + else + { + SHARE_INFO_2 si = (SHARE_INFO_2)Marshal.PtrToStructure(pItem, t); + shares.Add(si.NetName, si.Path, si.ShareType, si.Remark); + } + } + } + + } + finally + { + // Clean up buffer allocated by system + if (IntPtr.Zero != pBuffer) + NetApiBufferFree(pBuffer); + } + } + + /// + /// Enumerates the shares on Windows 9x + /// + /// The server name + /// The ShareCollection + protected static void EnumerateShares9x(string server, ShareCollection shares) + { + int level = 50; + int nRet = 0; + ushort entriesRead, totalEntries; + + Type t = typeof(SHARE_INFO_50); + int size = Marshal.SizeOf(t); + ushort cbBuffer = (ushort)(MAX_SI50_ENTRIES * size); + //On Win9x, must allocate buffer before calling API + IntPtr pBuffer = Marshal.AllocHGlobal(cbBuffer); + + try + { + nRet = NetShareEnum(server, level, pBuffer, cbBuffer, + out entriesRead, out totalEntries); + + if (ERROR_WRONG_LEVEL == nRet) + { + level = 1; + t = typeof(SHARE_INFO_1_9x); + size = Marshal.SizeOf(t); + + nRet = NetShareEnum(server, level, pBuffer, cbBuffer, + out entriesRead, out totalEntries); + } + + if (NO_ERROR == nRet || ERROR_MORE_DATA == nRet) + { + for (int i = 0, lpItem = pBuffer.ToInt32(); i < entriesRead; i++, lpItem += size) + { + IntPtr pItem = new IntPtr(lpItem); + + if (1 == level) + { + SHARE_INFO_1_9x si = (SHARE_INFO_1_9x)Marshal.PtrToStructure(pItem, t); + shares.Add(si.NetName, string.Empty, si.ShareType, si.Remark); + } + else + { + SHARE_INFO_50 si = (SHARE_INFO_50)Marshal.PtrToStructure(pItem, t); + shares.Add(si.NetName, si.Path, si.ShareType, si.Remark); + } + } + } + else + Console.WriteLine(nRet); + + } + finally + { + //Clean up buffer + Marshal.FreeHGlobal(pBuffer); + } + } + + /// + /// Enumerates the shares + /// + /// The server name + /// The ShareCollection + protected static void EnumerateShares(string server, ShareCollection shares) + { + if (null != server && 0 != server.Length && !IsW2KUp) + { + server = server.ToUpper(); + + // On NT4, 9x and Me, server has to start with "\\" + if (!('\\' == server[0] && '\\' == server[1])) + server = @"\\" + server; + } + + if (IsNT) + EnumerateSharesNT(server, shares); + else + EnumerateShares9x(server, shares); + } + + #endregion + + #endregion + + #region Static methods + + /// + /// Returns true if fileName is a valid local file-name of the form: + /// X:\, where X is a drive letter from A-Z + /// + /// The filename to check + /// + public static bool IsValidFilePath(string fileName) + { + if (null == fileName || 0 == fileName.Length) return false; + + char drive = char.ToUpper(fileName[0]); + if ('A' > drive || drive > 'Z') + return false; + + else if (Path.VolumeSeparatorChar != fileName[1]) + return false; + else if (Path.DirectorySeparatorChar != fileName[2]) + return false; + else + return true; + } + + #endregion + + /// The name of the server this collection represents + private string _server; + + #region Constructor + + /// + /// Default constructor - local machine + /// + public ShareCollection() + { + _server = string.Empty; + EnumerateShares(_server, this); + } + + /// + /// Constructor + /// + /// + public ShareCollection(string server) + { + _server = server; + EnumerateShares(_server, this); + } + + #endregion + + #region Add + + protected void Add(Share share) + { + InnerList.Add(share); + } + + protected void Add(string netName, string path, ShareType shareType, string remark) + { + InnerList.Add(new Share(_server, netName, path, shareType, remark)); + } + + #endregion + + #region Properties + + /// + /// Returns the name of the server this collection represents + /// + public string Server + { + get { return _server; } + } + + /// + /// Returns the at the specified index. + /// + public Share this[int index] + { + get { return (Share)InnerList[index]; } + } + + /// + /// Returns the which matches a given local path + /// + /// The path to match + public Share this[string path] + { + get + { + if (null == path || 0 == path.Length) return null; + + path = Path.GetFullPath(path); + if (!IsValidFilePath(path)) return null; + + Share match = null; + + for (int i = 0; i < InnerList.Count; i++) + { + Share s = (Share)InnerList[i]; + + if (s.IsFileSystem && s.MatchesPath(path)) + { + //Store first match + if (null == match) + match = s; + + // If this has a longer path, + // and this is a disk share or match is a special share, + // then this is a better match + else if (match.Path.Length < s.Path.Length) + { + if (ShareType.Disk == s.ShareType || ShareType.Disk != match.ShareType) + match = s; + } + } + } + + return match; + } + } + + #endregion + + /// + /// Copy this collection to an array + /// + /// + /// + public void CopyTo(Share[] array, int index) + { + InnerList.CopyTo(array, index); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Library/LibraryManager.cs b/MediaBrowser.Controller/Library/LibraryManager.cs index 95a11e8fe..4087f9ef8 100644 --- a/MediaBrowser.Controller/Library/LibraryManager.cs +++ b/MediaBrowser.Controller/Library/LibraryManager.cs @@ -369,7 +369,7 @@ namespace MediaBrowser.Controller.Library /// The cancellation token. /// The progress. /// Task. - internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) + internal async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) { // Clear the IBN cache ImagesByNameItemCache.Clear(); @@ -422,14 +422,14 @@ namespace MediaBrowser.Controller.Library double percent = numComplete; percent /= people.Count; - progress.Report(new TaskProgress { PercentComplete = 100 * percent }); + progress.Report(100 * percent); } })); } await Task.WhenAll(tasks).ConfigureAwait(false); - progress.Report(new TaskProgress { PercentComplete = 100 }); + progress.Report(100); _logger.Info("People validation complete"); } @@ -440,17 +440,17 @@ namespace MediaBrowser.Controller.Library /// The progress. /// The cancellation token. /// Task. - internal async Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) + internal async Task ValidateMediaLibrary(IProgress progress, CancellationToken cancellationToken) { _logger.Info("Validating media library"); await Kernel.RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); // Start by just validating the children of the root, but go no further - await Kernel.RootFolder.ValidateChildren(new Progress { }, cancellationToken, recursive: false); + await Kernel.RootFolder.ValidateChildren(new Progress { }, cancellationToken, recursive: false); // Validate only the collection folders for each user, just to make them available as quickly as possible - var userCollectionFolderTasks = Kernel.Users.AsParallel().Select(user => user.ValidateCollectionFolders(new Progress { }, cancellationToken)); + var userCollectionFolderTasks = Kernel.Users.AsParallel().Select(user => user.ValidateCollectionFolders(new Progress { }, cancellationToken)); await Task.WhenAll(userCollectionFolderTasks).ConfigureAwait(false); // Now validate the entire media library @@ -458,7 +458,7 @@ namespace MediaBrowser.Controller.Library foreach (var user in Kernel.Users) { - await user.ValidateMediaLibrary(new Progress { }, cancellationToken).ConfigureAwait(false); + await user.ValidateMediaLibrary(new Progress { }, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index c2b11ae32..0bf3e6c1f 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -67,7 +67,6 @@ - @@ -106,6 +105,7 @@ + diff --git a/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs index 21f1bce5a..d3d30ed77 100644 --- a/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Controller/ScheduledTasks/ChapterImagesTask.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.ScheduledTasks /// The cancellation token. /// The progress. /// Task. - protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress progress) + protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress progress) { var videos = Kernel.RootFolder.RecursiveChildren.OfType