Browse Source

close ffmpeg more gracefully

Luke Pulverenti 11 years ago
parent
commit
4398393783

+ 37 - 39
MediaBrowser.Api/ApiEntryPoint.cs

@@ -3,15 +3,14 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Session;
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Model.Session;
 
 namespace MediaBrowser.Api
 {
@@ -100,7 +99,7 @@ namespace MediaBrowser.Api
         {
             var jobCount = _activeTranscodingJobs.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs.ToList(), KillTranscodingJob);
+            Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, true));
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
@@ -291,16 +290,16 @@ namespace MediaBrowser.Api
         {
             var job = (TranscodingJob)state;
 
-            KillTranscodingJob(job);
+            KillTranscodingJob(job, true);
         }
 
         /// <summary>
         /// Kills the single transcoding job.
         /// </summary>
         /// <param name="deviceId">The device id.</param>
-        /// <param name="isVideo">if set to <c>true</c> [is video].</param>
+        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
         /// <exception cref="System.ArgumentNullException">sourcePath</exception>
-        internal void KillTranscodingJobs(string deviceId, bool isVideo)
+        internal void KillTranscodingJobs(string deviceId, bool deleteFiles)
         {
             if (string.IsNullOrEmpty(deviceId))
             {
@@ -318,7 +317,7 @@ namespace MediaBrowser.Api
 
             foreach (var job in jobs)
             {
-                KillTranscodingJob(job);
+                KillTranscodingJob(job, deleteFiles);
             }
         }
 
@@ -326,7 +325,8 @@ namespace MediaBrowser.Api
         /// Kills the transcoding job.
         /// </summary>
         /// <param name="job">The job.</param>
-        private void KillTranscodingJob(TranscodingJob job)
+        /// <param name="deleteFiles">if set to <c>true</c> [delete files].</param>
+        private void KillTranscodingJob(TranscodingJob job, bool deleteFiles)
         {
             lock (_activeTranscodingJobs)
             {
@@ -344,48 +344,44 @@ namespace MediaBrowser.Api
                 }
             }
 
-            var process = job.Process;
-
-            var hasExited = true;
-
-            try
+            lock (job.ProcessLock)
             {
-                hasExited = process.HasExited;
-            }
-            catch (Exception ex)
-            {
-                Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
-            }
-
-            if (!hasExited)
-            {
-                try
-                {
-                    Logger.Info("Killing ffmpeg process for {0}", job.Path);
+                var process = job.Process;
 
-                    process.Kill();
+                var hasExited = true;
 
-                    // Need to wait because killing is asynchronous
-                    process.WaitForExit(5000);
-                }
-                catch (Win32Exception ex)
+                try
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    hasExited = process.HasExited;
                 }
-                catch (InvalidOperationException ex)
+                catch (Exception ex)
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
                 }
-                catch (NotSupportedException ex)
+
+                if (!hasExited)
                 {
-                    Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    try
+                    {
+                        Logger.Info("Killing ffmpeg process for {0}", job.Path);
+
+                        //process.Kill();
+                        process.StandardInput.WriteLine("q");
+
+                        // Need to wait because killing is asynchronous
+                        process.WaitForExit(5000);
+                    }
+                    catch (Exception ex)
+                    {
+                        Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path);
+                    }
                 }
             }
 
-            // Dispose the process
-            process.Dispose();
-
-            DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
+            if (deleteFiles)
+            {
+                DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
+            }
         }
 
         private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
@@ -494,6 +490,8 @@ namespace MediaBrowser.Api
         public string DeviceId { get; set; }
 
         public CancellationTokenSource CancellationTokenSource { get; set; }
+
+        public object ProcessLock = new object();
     }
 
     /// <summary>

+ 15 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -816,6 +816,7 @@ namespace MediaBrowser.Api.Playback
                     // Must consume both stdout and stderr or deadlocks may occur
                     RedirectStandardOutput = true,
                     RedirectStandardError = true,
+                    RedirectStandardInput = true,
 
                     FileName = MediaEncoder.EncoderPath,
                     WorkingDirectory = Path.GetDirectoryName(MediaEncoder.EncoderPath),
@@ -1073,8 +1074,9 @@ namespace MediaBrowser.Api.Playback
         /// </summary>
         /// <param name="process">The process.</param>
         /// <param name="state">The state.</param>
-        protected void OnFfMpegProcessExited(Process process, StreamState state)
+        private void OnFfMpegProcessExited(Process process, StreamState state)
         {
+            Logger.Debug("Disposing stream resources");
             state.Dispose();
 
             try
@@ -1083,8 +1085,19 @@ namespace MediaBrowser.Api.Playback
             }
             catch
             {
-                Logger.Info("FFMpeg exited with an error.");
+                Logger.Error("FFMpeg exited with an error.");
             }
+
+            // This causes on exited to be called twice:
+            //try
+            //{
+            //    // Dispose the process
+            //    process.Dispose();
+            //}
+            //catch (Exception ex)
+            //{
+            //    Logger.ErrorException("Error disposing ffmpeg.", ex);
+            //}
         }
 
         protected double? GetFramerateParam(StreamState state)

+ 2 - 2
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -83,7 +83,7 @@ namespace MediaBrowser.Api.Playback.Hls
         {
             var cancellationTokenSource = new CancellationTokenSource();
 
-            var state = GetState(request, cancellationTokenSource.Token).Result;
+            var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
 
             var playlist = state.OutputFilePath;
 
@@ -154,7 +154,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.Int32.</returns>
         protected int GetSegmentWait()
         {
-            var minimumSegmentCount = 3;
+            var minimumSegmentCount = 2;
             var quality = GetQualitySetting();
 
             if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality)

+ 60 - 16
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -1,13 +1,10 @@
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
@@ -17,7 +14,6 @@ using System.IO;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using MimeTypes = ServiceStack.MimeTypes;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -83,29 +79,75 @@ namespace MediaBrowser.Api.Playback.Hls
             return GetDynamicSegment(request, true).Result;
         }
 
+        private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
         private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
         {
+            var cancellationTokenSource = new CancellationTokenSource();
+            var cancellationToken = cancellationTokenSource.Token;
+
             var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
 
-            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+            var state = await GetState(request, cancellationToken).ConfigureAwait(false);
 
             var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
 
-            var path = GetSegmentPath(playlistPath, index);
+            var segmentPath = GetSegmentPath(playlistPath, index);
 
-            if (File.Exists(path))
+            if (File.Exists(segmentPath))
             {
-                return GetSegementResult(path);
+                ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+                return GetSegementResult(segmentPath);
             }
 
-            if (!File.Exists(playlistPath))
+            await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
+            try
+            {
+                if (File.Exists(segmentPath))
+                {
+                    ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
+                    return GetSegementResult(segmentPath);
+                }
+                else
+                {
+                    if (index == 0)
+                    {
+                        // If the playlist doesn't already exist, startup ffmpeg
+                        try
+                        {
+                            ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false);
+
+                            await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
+                        }
+                        catch
+                        {
+                            state.Dispose();
+                            throw;
+                        }
+
+                        await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
+                    }
+                }
+            }
+            finally
             {
-                await StartFfMpeg(state, playlistPath, new CancellationTokenSource()).ConfigureAwait(false);
+                FfmpegStartLock.Release();
+            }
 
-                await WaitForMinimumSegmentCount(playlistPath, GetSegmentWait(), CancellationToken.None).ConfigureAwait(false);
+            Logger.Info("waiting for {0}", segmentPath);
+            while (!File.Exists(segmentPath))
+            {
+                await Task.Delay(50, cancellationToken).ConfigureAwait(false);
             }
 
-            return GetSegementResult(path);
+            Logger.Info("returning {0}", segmentPath);
+            return GetSegementResult(segmentPath);
+        }
+
+        protected override int GetStartNumber(StreamState state)
+        {
+            var request = (GetDynamicHlsVideoSegment) state.Request;
+
+            return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
         }
 
         private string GetSegmentPath(string playlist, int index)
@@ -120,7 +162,7 @@ namespace MediaBrowser.Api.Playback.Hls
         private object GetSegementResult(string path)
         {
             // TODO: Handle if it's currently being written to
-            return ResultFactory.GetStaticFileResult(Request, path);
+            return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
         }
 
         private async Task<object> GetAsync(GetMasterHlsVideoStream request)
@@ -143,7 +185,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
 
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+            return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
 
         private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
@@ -226,7 +268,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var playlistText = builder.ToString();
 
-            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+            return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
         }
 
         protected override string GetAudioArguments(StreamState state)
@@ -274,7 +316,9 @@ namespace MediaBrowser.Api.Playback.Hls
                 return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
             }
 
-            const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = state.ReadInputAtNativeFramerate ?
+                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
+                " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
 
             var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
 

+ 1 - 4
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -1,19 +1,16 @@
-using System.Threading;
 using MediaBrowser.Common.IO;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
 using System.IO;
 using System.Linq;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls

+ 1 - 1
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -134,7 +134,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         /// <returns>System.String.</returns>
         public string GetProbeSizeArgument(string[] inputFiles, MediaProtocol protocol)
         {
-            return EncodingUtils.GetProbeSizeArgument(inputFiles.Length > 0);
+            return EncodingUtils.GetProbeSizeArgument(inputFiles.Length > 1);
         }
 
         /// <summary>

+ 4 - 2
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -54,12 +54,14 @@ namespace MediaBrowser.Model.Dlna
                 // Avoid implicitly captured closure
                 string mediaSourceId = options.MediaSourceId;
 
-                mediaSources = new List<MediaSourceInfo>();
+                var newMediaSources = new List<MediaSourceInfo>();
                 foreach (MediaSourceInfo i in mediaSources)
                 {
                     if (StringHelper.EqualsIgnoreCase(i.Id, mediaSourceId))
-                        mediaSources.Add(i);
+                        newMediaSources.Add(i);
                 }
+
+                mediaSources = newMediaSources;
             }
 
             List<StreamInfo> streams = new List<StreamInfo>();

+ 170 - 26
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Common.Extensions;
+using System.Collections.Concurrent;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
@@ -23,7 +24,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Server.Implementations.Channels
 {
-    public class ChannelManager : IChannelManager
+    public class ChannelManager : IChannelManager, IDisposable
     {
         private IChannel[] _channels;
         private IChannelFactory[] _factories;
@@ -39,6 +40,9 @@ namespace MediaBrowser.Server.Implementations.Channels
         private readonly IJsonSerializer _jsonSerializer;
 
         private readonly ILocalizationManager _localization;
+        private readonly ConcurrentDictionary<Guid, bool> _refreshedItems = new ConcurrentDictionary<Guid, bool>();
+
+        private Timer _refreshTimer;
 
         public ChannelManager(IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, ILocalizationManager localization)
         {
@@ -51,6 +55,8 @@ namespace MediaBrowser.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _jsonSerializer = jsonSerializer;
             _localization = localization;
+
+            _refreshTimer = new Timer(s => _refreshedItems.Clear(), null, TimeSpan.FromHours(3), TimeSpan.FromHours(3));
         }
 
         private TimeSpan CacheLength
@@ -203,8 +209,8 @@ namespace MediaBrowser.Server.Implementations.Channels
 
             if (requiresCallback != null)
             {
-                results = await requiresCallback.GetChannelItemMediaInfo(item.ExternalId, cancellationToken)
-                   .ConfigureAwait(false);
+                results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
+                            .ConfigureAwait(false);
             }
             else
             {
@@ -221,6 +227,31 @@ namespace MediaBrowser.Server.Implementations.Channels
             return sources;
         }
 
+        private readonly ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>> _channelItemMediaInfo =
+            new ConcurrentDictionary<string, Tuple<DateTime, List<ChannelMediaInfo>>>();
+
+        private async Task<IEnumerable<ChannelMediaInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
+        {
+            Tuple<DateTime, List<ChannelMediaInfo>> cachedInfo;
+
+            if (_channelItemMediaInfo.TryGetValue(id, out cachedInfo))
+            {
+                if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
+                {
+                    return cachedInfo.Item2;
+                }
+            }
+
+            var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
+                   .ConfigureAwait(false);
+            var list = mediaInfo.ToList();
+
+            var item2 = new Tuple<DateTime, List<ChannelMediaInfo>>(DateTime.UtcNow, list);
+            _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
+
+            return list;
+        }
+
         public IEnumerable<MediaSourceInfo> GetCachedChannelItemMediaSources(string id)
         {
             var item = (IChannelMediaItem)_libraryManager.GetItemById(id);
@@ -515,11 +546,7 @@ namespace MediaBrowser.Server.Implementations.Channels
                     {
                         try
                         {
-                            var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch
-                            {
-                                UserId = userId
-
-                            }, cancellationToken).ConfigureAwait(false);
+                            var result = await GetLatestItems(indexable, i, userId, cancellationToken).ConfigureAwait(false);
 
                             var resultItems = result.ToList();
 
@@ -585,6 +612,65 @@ namespace MediaBrowser.Server.Implementations.Channels
             };
         }
 
+        private async Task<IEnumerable<ChannelItemInfo>> GetLatestItems(ISupportsLatestMedia indexable, IChannel channel, string userId, CancellationToken cancellationToken)
+        {
+            var cacheLength = TimeSpan.FromHours(12);
+            var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-latest", null, false);
+
+            try
+            {
+                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                {
+                    return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+
+            }
+            catch (DirectoryNotFoundException)
+            {
+
+            }
+
+            await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                try
+                {
+                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                    {
+                        return _jsonSerializer.DeserializeFromFile<List<ChannelItemInfo>>(cachePath);
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+
+                }
+                catch (DirectoryNotFoundException)
+                {
+
+                }
+
+                var result = await indexable.GetLatestMedia(new ChannelLatestMediaSearch
+                {
+                    UserId = userId
+
+                }, cancellationToken).ConfigureAwait(false);
+
+                var resultItems = result.ToList();
+
+                CacheResponse(resultItems, cachePath);
+
+                return resultItems;
+            }
+            finally
+            {
+                _resourcePool.Release();
+            }
+        }
+
         public async Task<QueryResult<BaseItemDto>> GetAllMedia(AllChannelMediaQuery query, CancellationToken cancellationToken)
         {
             var user = string.IsNullOrWhiteSpace(query.UserId)
@@ -614,11 +700,7 @@ namespace MediaBrowser.Server.Implementations.Channels
                     {
                         try
                         {
-                            var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery
-                            {
-                                UserId = userId
-
-                            }, cancellationToken).ConfigureAwait(false);
+                            var result = await GetAllItems(indexable, i, userId, cancellationToken).ConfigureAwait(false);
 
                             return new Tuple<IChannel, ChannelItemResult>(i, result);
                         }
@@ -677,6 +759,63 @@ namespace MediaBrowser.Server.Implementations.Channels
             };
         }
 
+        private async Task<ChannelItemResult> GetAllItems(IIndexableChannel indexable, IChannel channel, string userId, CancellationToken cancellationToken)
+        {
+            var cacheLength = TimeSpan.FromHours(12);
+            var cachePath = GetChannelDataCachePath(channel, userId, "channelmanager-allitems", null, false);
+
+            try
+            {
+                if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                {
+                    return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+                }
+            }
+            catch (FileNotFoundException)
+            {
+
+            }
+            catch (DirectoryNotFoundException)
+            {
+
+            }
+
+            await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            try
+            {
+                try
+                {
+                    if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
+                    {
+                        return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+
+                }
+                catch (DirectoryNotFoundException)
+                {
+
+                }
+
+                var result = await indexable.GetAllMedia(new InternalAllChannelMediaQuery
+                {
+                    UserId = userId
+
+                }, cancellationToken).ConfigureAwait(false);
+
+                CacheResponse(result, cachePath);
+
+                return result;
+            }
+            finally
+            {
+                _resourcePool.Release();
+            }
+        }
+
         public async Task<QueryResult<BaseItemDto>> GetChannelItems(ChannelItemQuery query, CancellationToken cancellationToken)
         {
             var queryChannelId = query.ChannelId;
@@ -764,11 +903,9 @@ namespace MediaBrowser.Server.Implementations.Channels
             {
                 if (!startIndex.HasValue && !limit.HasValue)
                 {
-                    var channelItemResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
-
                     if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                     {
-                        return channelItemResult;
+                        return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
                     }
                 }
             }
@@ -789,11 +926,9 @@ namespace MediaBrowser.Server.Implementations.Channels
                 {
                     if (!startIndex.HasValue && !limit.HasValue)
                     {
-                        var channelItemResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
-
                         if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
                         {
-                            return channelItemResult;
+                            return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
                         }
                     }
                 }
@@ -837,7 +972,7 @@ namespace MediaBrowser.Server.Implementations.Channels
             }
         }
 
-        private void CacheResponse(ChannelItemResult result, string path)
+        private void CacheResponse(object result, string path)
         {
             try
             {
@@ -993,8 +1128,8 @@ namespace MediaBrowser.Server.Implementations.Channels
                 item.ProductionYear = info.ProductionYear;
                 item.ProviderIds = info.ProviderIds;
 
-                item.DateCreated = info.DateCreated.HasValue ? 
-                    info.DateCreated.Value : 
+                item.DateCreated = info.DateCreated.HasValue ?
+                    info.DateCreated.Value :
                     DateTime.UtcNow;
             }
 
@@ -1042,14 +1177,14 @@ namespace MediaBrowser.Server.Implementations.Channels
 
         private async Task RefreshIfNeeded(BaseItem program, CancellationToken cancellationToken)
         {
-            //if (_refreshedPrograms.ContainsKey(program.Id))
+            if (_refreshedItems.ContainsKey(program.Id))
             {
-                //return;
+                return;
             }
 
             await program.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 
-            //_refreshedPrograms.TryAdd(program.Id, true);
+            _refreshedItems.TryAdd(program.Id, true);
         }
 
         internal IChannel GetChannelProvider(Channel channel)
@@ -1155,5 +1290,14 @@ namespace MediaBrowser.Server.Implementations.Channels
             var name = _localization.GetLocalizedString("ViewTypeChannels");
             return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false);
         }
+
+        public void Dispose()
+        {
+            if (_refreshTimer != null)
+            {
+                _refreshTimer.Dispose();
+                _refreshTimer = null;
+            }
+        }
     }
 }

+ 2 - 2
MediaBrowser.Server.Implementations/Library/LibraryManager.cs

@@ -1497,7 +1497,7 @@ namespace MediaBrowser.Server.Implementations.Library
 
         public async Task<UserView> GetNamedView(string name, string type, string sortName, CancellationToken cancellationToken)
         {
-            var id = "namedview_2_" + name;
+            var id = "namedview_3_" + name;
             var guid = id.GetMD5();
 
             var item = GetItemById(guid) as UserView;
@@ -1506,7 +1506,7 @@ namespace MediaBrowser.Server.Implementations.Library
             {
                 var path = Path.Combine(ConfigurationManager.ApplicationPaths.ItemsByNamePath,
                     "views",
-                    _fileSystem.GetValidFilename(name));
+                    _fileSystem.GetValidFilename(type));
 
                 Directory.CreateDirectory(Path.GetDirectoryName(path));
 

+ 12 - 3
MediaBrowser.ServerApplication/MainStartup.cs

@@ -527,18 +527,27 @@ namespace MediaBrowser.ServerApplication
 
             if (!_isRunningAsService)
             {
-                _logger.Info("Executing windows forms restart");
+                _logger.Info("Hiding server notify icon");
                 _serverNotifyIcon.Visible = false;
-                Application.Restart();
 
-                ShutdownWindowsApplication();
+                _logger.Info("Executing windows forms restart");
+                //Application.Restart();
+                Process.Start(_appHost.ServerConfigurationManager.ApplicationPaths.ApplicationPath);
+
+                _logger.Info("Calling Application.Exit");
+                Environment.Exit(0);
             }
         }
 
         private static void ShutdownWindowsApplication()
         {
+            _logger.Info("Hiding server notify icon");
             _serverNotifyIcon.Visible = false;
+
+            _logger.Info("Calling Application.Exit");
             Application.Exit();
+
+            _logger.Info("Calling ApplicationTaskCompletionSource.SetResult");
             ApplicationTaskCompletionSource.SetResult(true);
         }
 

+ 2 - 2
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -311,6 +311,7 @@ namespace MediaBrowser.WebDashboard.Api
         /// Modifies the HTML by adding common meta tags, css and js.
         /// </summary>
         /// <param name="sourceStream">The source stream.</param>
+        /// <param name="userId">The user identifier.</param>
         /// <param name="localizationCulture">The localization culture.</param>
         /// <returns>Task{Stream}.</returns>
         private async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture)
@@ -373,8 +374,7 @@ namespace MediaBrowser.WebDashboard.Api
             sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
             sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
             sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
-            //sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
-            //sb.Append("<meta name=\"msapplication-config\" content=\"config.xml\">");
+            sb.Append("<meta name=\"application-name\" content=\"Media Browser\">");
             //sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
 
             sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

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

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

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

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

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