Преглед на файлове

Merge pull request #2458 from MediaBrowser/beta

Beta
Luke преди 8 години
родител
ревизия
f1c7b86081
променени са 32 файла, в които са добавени 338 реда и са изтрити 345 реда
  1. 25 1
      Emby.Common.Implementations/BaseApplicationHost.cs
  2. 1 1
      Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs
  3. 35 18
      Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs
  4. 1 2
      Emby.Dlna/Profiles/WdtvLiveProfile.cs
  5. 1 1
      Emby.Dlna/Profiles/Xml/WDTV Live.xml
  6. 9 37
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  7. 2 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  8. 12 0
      Emby.Server.Implementations/Library/LibraryManager.cs
  9. 10 11
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  10. 15 13
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  11. 0 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  12. 26 8
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  13. 13 1
      Emby.Server.Implementations/LiveTv/LiveStreamHelper.cs
  14. 10 5
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  15. 4 1
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  16. 9 6
      Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs
  17. 1 1
      Emby.Server.Implementations/packages.config
  18. 1 1
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  19. 1 0
      MediaBrowser.Api/StartupWizardService.cs
  20. 1 0
      MediaBrowser.Common/Net/HttpRequestOptions.cs
  21. 1 1
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  22. 0 5
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  23. 12 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  24. 0 4
      MediaBrowser.Model/Entities/MetadataProviders.cs
  25. 2 1
      MediaBrowser.Model/System/IEnvironmentInfo.cs
  26. 1 1
      MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs
  27. 11 6
      MediaBrowser.Providers/Omdb/OmdbProvider.cs
  28. 13 0
      MediaBrowser.Server.Mono/Program.cs
  29. 56 51
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  30. 14 36
      MediaBrowser.WebDashboard/Api/PackageCreator.cs
  31. 0 8
      MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
  32. 51 122
      RSSDP/SsdpDeviceLocatorBase.cs

+ 25 - 1
Emby.Common.Implementations/BaseApplicationHost.cs

@@ -527,7 +527,7 @@ return null;
 
 
             RegisterSingleInstance(FileSystemManager);
             RegisterSingleInstance(FileSystemManager);
 
 
-            HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory);
+            HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory, GetDefaultUserAgent);
             RegisterSingleInstance(HttpClient);
             RegisterSingleInstance(HttpClient);
 
 
             RegisterSingleInstance(NetworkManager);
             RegisterSingleInstance(NetworkManager);
@@ -549,6 +549,30 @@ return null;
             return Task.FromResult(true);
             return Task.FromResult(true);
         }
         }
 
 
+        private string GetDefaultUserAgent()
+        {
+            var name = FormatAttribute(Name);
+
+            return name + "/" + ApplicationVersion.ToString();
+        }
+
+        private string FormatAttribute(string str)
+        {
+            var arr = str.ToCharArray();
+
+            arr = Array.FindAll<char>(arr, (c => (char.IsLetterOrDigit(c)
+                                              || char.IsWhiteSpace(c))));
+
+            var result = new string(arr);
+
+            if (string.IsNullOrWhiteSpace(result))
+            {
+                result = "Emby";
+            }
+
+            return result;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets a list of types within an assembly
         /// Gets a list of types within an assembly
         /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference
         /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference

+ 1 - 1
Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs

@@ -12,7 +12,7 @@ namespace Emby.Common.Implementations.EnvironmentInfo
         public MediaBrowser.Model.System.Architecture? CustomArchitecture { get; set; }
         public MediaBrowser.Model.System.Architecture? CustomArchitecture { get; set; }
         public MediaBrowser.Model.System.OperatingSystem? CustomOperatingSystem { get; set; }
         public MediaBrowser.Model.System.OperatingSystem? CustomOperatingSystem { get; set; }
 
 
-        public MediaBrowser.Model.System.OperatingSystem OperatingSystem
+        public virtual MediaBrowser.Model.System.OperatingSystem OperatingSystem
         {
         {
             get
             get
             {
             {

+ 35 - 18
Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -18,6 +18,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Common.Implementations.HttpClientManager;
 using Emby.Common.Implementations.HttpClientManager;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Common;
 
 
 namespace Emby.Common.Implementations.HttpClientManager
 namespace Emby.Common.Implementations.HttpClientManager
 {
 {
@@ -43,17 +44,12 @@ namespace Emby.Common.Implementations.HttpClientManager
 
 
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly IMemoryStreamFactory _memoryStreamProvider;
         private readonly IMemoryStreamFactory _memoryStreamProvider;
+        private readonly Func<string> _defaultUserAgentFn;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
         /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
         /// </summary>
         /// </summary>
-        /// <param name="appPaths">The app paths.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <exception cref="System.ArgumentNullException">appPaths
-        /// or
-        /// logger</exception>
-        public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider)
+        public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider, Func<string> defaultUserAgentFn)
         {
         {
             if (appPaths == null)
             if (appPaths == null)
             {
             {
@@ -68,6 +64,7 @@ namespace Emby.Common.Implementations.HttpClientManager
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _memoryStreamProvider = memoryStreamProvider;
             _memoryStreamProvider = memoryStreamProvider;
             _appPaths = appPaths;
             _appPaths = appPaths;
+            _defaultUserAgentFn = defaultUserAgentFn;
 
 
 #if NET46
 #if NET46
             // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
             // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
@@ -257,6 +254,8 @@ namespace Emby.Common.Implementations.HttpClientManager
 
 
         private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
         private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
         {
         {
+            var hasUserAgent = false;
+
             foreach (var header in options.RequestHeaders.ToList())
             foreach (var header in options.RequestHeaders.ToList())
             {
             {
                 if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase))
                 if (string.Equals(header.Key, "Accept", StringComparison.OrdinalIgnoreCase))
@@ -265,11 +264,8 @@ namespace Emby.Common.Implementations.HttpClientManager
                 }
                 }
                 else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase))
                 else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase))
                 {
                 {
-#if NET46
-                    request.UserAgent = header.Value;
-#elif NETSTANDARD1_6
-                    request.Headers["User-Agent"] = header.Value;
-#endif
+                    SetUserAgent(request, header.Value);
+                    hasUserAgent = true;
                 }
                 }
                 else
                 else
                 {
                 {
@@ -280,6 +276,20 @@ namespace Emby.Common.Implementations.HttpClientManager
 #endif
 #endif
                 }
                 }
             }
             }
+
+            if (!hasUserAgent && options.EnableDefaultUserAgent)
+            {
+                SetUserAgent(request, _defaultUserAgentFn());
+            }
+        }
+
+        private void SetUserAgent(HttpWebRequest request, string userAgent)
+        {
+#if NET46
+            request.UserAgent = userAgent;
+#elif NETSTANDARD1_6
+                    request.Headers["User-Agent"] = userAgent;
+#endif
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -448,15 +458,22 @@ namespace Emby.Common.Implementations.HttpClientManager
                 !string.IsNullOrEmpty(options.RequestContent) ||
                 !string.IsNullOrEmpty(options.RequestContent) ||
                 string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
                 string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
             {
             {
-                var bytes = options.RequestContentBytes ??
-                    Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
+                try
+                {
+                    var bytes = options.RequestContentBytes ??
+                        Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
 
 
-                httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
+                    httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
 
 
 #if NET46
 #if NET46
-                httpWebRequest.ContentLength = bytes.Length;
-#endif    
-                (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
+                    httpWebRequest.ContentLength = bytes.Length;
+#endif
+                    (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
+                }
+                catch (Exception ex)
+                {
+                    throw new HttpException(ex.Message) { IsTimedOut = true };
+                }
             }
             }
 
 
             if (options.ResourcePool != null)
             if (options.ResourcePool != null)

+ 1 - 2
Emby.Dlna/Profiles/WdtvLiveProfile.cs

@@ -125,8 +125,7 @@ namespace Emby.Dlna.Profiles
 
 
                 new DirectPlayProfile
                 new DirectPlayProfile
                 {
                 {
-                    Container = "flac",
-                    AudioCodec = "flac",
+                    Container = "flac,ac3",
                     Type = DlnaProfileType.Audio
                     Type = DlnaProfileType.Audio
                 },
                 },
 
 

+ 1 - 1
Emby.Dlna/Profiles/Xml/WDTV Live.xml

@@ -45,7 +45,7 @@
     <DirectPlayProfile container="asf" audioCodec="mp2,ac3" videoCodec="mpeg2video" type="Video" />
     <DirectPlayProfile container="asf" audioCodec="mp2,ac3" videoCodec="mpeg2video" type="Video" />
     <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
     <DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />
     <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
     <DirectPlayProfile container="mp4" audioCodec="mp4" type="Audio" />
-    <DirectPlayProfile container="flac" audioCodec="flac" type="Audio" />
+    <DirectPlayProfile container="flac,ac3" type="Audio" />
     <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
     <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" />
     <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
     <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" />
     <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />
     <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" />

+ 9 - 37
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -19,13 +19,12 @@ using Rssdp.Infrastructure;
 
 
 namespace Emby.Dlna.Ssdp
 namespace Emby.Dlna.Ssdp
 {
 {
-    public class DeviceDiscovery : IDeviceDiscovery, IDisposable
+    public class DeviceDiscovery : IDeviceDiscovery
     {
     {
         private bool _disposed;
         private bool _disposed;
 
 
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
-        private readonly CancellationTokenSource _tokenSource;
 
 
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered;
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
         public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
@@ -37,8 +36,6 @@ namespace Emby.Dlna.Ssdp
 
 
         public DeviceDiscovery(ILogger logger, IServerConfigurationManager config, ISocketFactory socketFactory, ITimerFactory timerFactory)
         public DeviceDiscovery(ILogger logger, IServerConfigurationManager config, ISocketFactory socketFactory, ITimerFactory timerFactory)
         {
         {
-            _tokenSource = new CancellationTokenSource();
-
             _logger = logger;
             _logger = logger;
             _config = config;
             _config = config;
             _socketFactory = socketFactory;
             _socketFactory = socketFactory;
@@ -59,39 +56,10 @@ namespace Emby.Dlna.Ssdp
             _deviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable;
             _deviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable;
             _deviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable;
             _deviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable;
 
 
-            // Perform a search so we don't have to wait for devices to broadcast notifications 
-            // again to get any results right away (notifications are broadcast periodically).
-            StartAsyncSearch();
-        }
-
-        private void StartAsyncSearch()
-        {
-            Task.Factory.StartNew(async (o) =>
-            {
-                while (!_tokenSource.IsCancellationRequested)
-                {
-                    try
-                    {
-                        // Enable listening for notifications (optional)
-                        _deviceLocator.StartListeningForNotifications();
-
-                        await _deviceLocator.SearchAsync(_tokenSource.Token).ConfigureAwait(false);
+            var dueTime = TimeSpan.FromSeconds(5);
+            var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds);
 
 
-                        var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000;
-
-                        await Task.Delay(delay, _tokenSource.Token).ConfigureAwait(false);
-                    }
-                    catch (OperationCanceledException)
-                    {
-
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.ErrorException("Error searching for devices", ex);
-                    }
-                }
-
-            }, CancellationToken.None, TaskCreationOptions.LongRunning);
+            _deviceLocator.RestartBroadcastTimer(dueTime, interval);
         }
         }
 
 
         // Process each found device in the event handler
         // Process each found device in the event handler
@@ -141,7 +109,11 @@ namespace Emby.Dlna.Ssdp
             if (!_disposed)
             if (!_disposed)
             {
             {
                 _disposed = true;
                 _disposed = true;
-                _tokenSource.Cancel();
+                if (_deviceLocator != null)
+                {
+                    _deviceLocator.Dispose();
+                    _deviceLocator = null;
+                }
             }
             }
         }
         }
     }
     }

+ 2 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -309,8 +309,8 @@
       <Project>{4f26d5d8-a7b0-42b3-ba42-7cb7d245934e}</Project>
       <Project>{4f26d5d8-a7b0-42b3-ba42-7cb7d245934e}</Project>
       <Name>SocketHttpListener.Portable</Name>
       <Name>SocketHttpListener.Portable</Name>
     </ProjectReference>
     </ProjectReference>
-    <Reference Include="Emby.XmlTv, Version=1.0.6241.4924, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\Emby.XmlTv.1.0.5\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
+    <Reference Include="Emby.XmlTv, Version=1.0.6249.32870, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\Emby.XmlTv.1.0.6\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
       <Private>True</Private>
       <Private>True</Private>
     </Reference>
     </Reference>
     <Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">
     <Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">

+ 12 - 0
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -2627,6 +2627,18 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             }
             }
 
 
+            foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
+            {
+                if (!string.IsNullOrWhiteSpace(map.From))
+                {
+                    var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
+                    if (substitutionResult.Item2)
+                    {
+                        return substitutionResult.Item1;
+                    }
+                }
+            }
+
             return path;
             return path;
         }
         }
 
 

+ 10 - 11
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -74,21 +74,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return new MusicArtist();
                 return new MusicArtist();
             }
             }
 
 
-            return null;
-            //if (_config.Configuration.EnableSimpleArtistDetection)
-            //{
-            //    return null;
-            //}
+            if (_config.Configuration.EnableSimpleArtistDetection)
+            {
+                return null;
+            }
 
 
-            //// Avoid mis-identifying top folders
-            //if (args.Parent.IsRoot) return null;
+            // Avoid mis-identifying top folders
+            if (args.Parent.IsRoot) return null;
 
 
-            //var directoryService = args.DirectoryService;
+            var directoryService = args.DirectoryService;
 
 
-            //var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+            var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
 
 
-            //// If we contain an album assume we are an artist folder
-            //return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
+            // If we contain an album assume we are an artist folder
+            return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService, args.GetLibraryOptions())) ? new MusicArtist() : null;
         }
         }
 
 
     }
     }

+ 15 - 13
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -423,6 +423,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     {
                     {
                         tunerChannel.Name = epgChannel.Name;
                         tunerChannel.Name = epgChannel.Name;
                     }
                     }
+                    if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl))
+                    {
+                        tunerChannel.ImageUrl = epgChannel.ImageUrl;
+                        tunerChannel.HasImage = true;
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -469,16 +474,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
         public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
         public ChannelInfo GetEpgChannelFromTunerChannel(List<NameValuePair> mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels)
         {
         {
-            if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
+            var tunerChannelId = string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId)
+                ? tunerChannel.Id
+                : tunerChannel.TunerChannelId;
+
+            if (!string.IsNullOrWhiteSpace(tunerChannelId))
             {
             {
-                var tunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings);
+                var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
 
 
-                if (string.IsNullOrWhiteSpace(tunerChannelId))
+                if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
                 {
                 {
-                    tunerChannelId = tunerChannel.TunerChannelId;
+                    mappedTunerChannelId = tunerChannelId;
                 }
                 }
 
 
-                var channel = epgChannels.FirstOrDefault(i => string.Equals(tunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
+                var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
 
 
                 if (channel != null)
                 if (channel != null)
                 {
                 {
@@ -1163,7 +1172,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 };
                 };
 
 
                 var isAudio = false;
                 var isAudio = false;
-                await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
+                await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, false, cancellationToken).ConfigureAwait(false);
 
 
                 return new List<MediaSourceInfo>
                 return new List<MediaSourceInfo>
                 {
                 {
@@ -2092,13 +2101,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                         writer.WriteElementString("credits", person);
                         writer.WriteElementString("credits", person);
                     }
                     }
 
 
-                    var rt = item.GetProviderId(MetadataProviders.RottenTomatoes);
-
-                    if (!string.IsNullOrEmpty(rt))
-                    {
-                        writer.WriteElementString("rottentomatoesid", rt);
-                    }
-
                     var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
                     var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
 
 
                     if (!string.IsNullOrEmpty(tmdbCollection))
                     if (!string.IsNullOrEmpty(tmdbCollection))

+ 0 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -155,7 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
             var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
             var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
             var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
             var mapArgs = string.Equals(OutputFormat, "mkv", StringComparison.OrdinalIgnoreCase) ? "-map 0" : "-sn";
             var mapArgs = string.Equals(OutputFormat, "mkv", StringComparison.OrdinalIgnoreCase) ? "-map 0" : "-sn";
-            // temporary
             mapArgs = "-sn";
             mapArgs = "-sn";
             var commandLineArgs = "-i \"{0}\"{4} " + mapArgs + " {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
             var commandLineArgs = "-i \"{0}\"{4} " + mapArgs + " {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
 
 

+ 26 - 8
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -152,7 +152,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                             responseString);
                             responseString);
                     var programDict = programDetails.ToDictionary(p => p.programID, y => y);
                     var programDict = programDetails.ToDictionary(p => p.programID, y => y);
 
 
-                    var images = await GetImageForPrograms(info, programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID).ToList(), cancellationToken);
+                    var programIdsWithImages =
+                        programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
+                        .ToList();
+
+                    var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken);
 
 
                     var schedules = dailySchedules.SelectMany(d => d.programs);
                     var schedules = dailySchedules.SelectMany(d => d.programs);
                     foreach (ScheduleDirect.Program schedule in schedules)
                     foreach (ScheduleDirect.Program schedule in schedules)
@@ -439,13 +443,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             List<string> programIds,
             List<string> programIds,
            CancellationToken cancellationToken)
            CancellationToken cancellationToken)
         {
         {
+            if (programIds.Count == 0)
+            {
+                return new List<ScheduleDirect.ShowImages>();
+            }
+
             var imageIdString = "[";
             var imageIdString = "[";
 
 
             foreach (var i in programIds)
             foreach (var i in programIds)
             {
             {
-                if (!imageIdString.Contains(i.Substring(0, 10)))
+                var imageId = i.Substring(0, 10);
+
+                if (!imageIdString.Contains(imageId))
                 {
                 {
-                    imageIdString += "\"" + i.Substring(0, 10) + "\",";
+                    imageIdString += "\"" + imageId + "\",";
                 }
                 }
             }
             }
 
 
@@ -461,14 +472,21 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 // The data can be large so give it some extra time
                 // The data can be large so give it some extra time
                 TimeoutMs = 60000
                 TimeoutMs = 60000
             };
             };
-            List<ScheduleDirect.ShowImages> images;
-            using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
+
+            try
             {
             {
-                images = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.ShowImages>>(
-                    innerResponse2.Content);
+                using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false))
+                {
+                    return _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.ShowImages>>(
+                        innerResponse2.Content);
+                }
             }
             }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error getting image info from schedules direct", ex);
 
 
-            return images;
+                return new List<ScheduleDirect.ShowImages>();
+            }
         }
         }
 
 
         public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken)
         public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken)

+ 13 - 1
Emby.Server.Implementations/LiveTv/LiveStreamHelper.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv
 namespace Emby.Server.Implementations.LiveTv
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.LiveTv
             _logger = logger;
             _logger = logger;
         }
         }
 
 
-        public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
+        public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, bool assumeInterlaced, CancellationToken cancellationToken)
         {
         {
             var originalRuntime = mediaSource.RunTimeTicks;
             var originalRuntime = mediaSource.RunTimeTicks;
 
 
@@ -95,6 +96,17 @@ namespace Emby.Server.Implementations.LiveTv
                 videoStream.IsAVC = null;
                 videoStream.IsAVC = null;
             }
             }
 
 
+            if (assumeInterlaced)
+            {
+                foreach (var mediaStream in mediaSource.MediaStreams)
+                {
+                    if (mediaStream.Type == MediaStreamType.Video)
+                    {
+                        mediaStream.IsInterlaced = true;
+                    }
+                }
+            }
+
             // Try to estimate this
             // Try to estimate this
             mediaSource.InferTotalBitrate(true);
             mediaSource.InferTotalBitrate(true);
         }
         }

+ 10 - 5
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -246,9 +246,9 @@ namespace Emby.Server.Implementations.LiveTv
             return info.Item1;
             return info.Item1;
         }
         }
 
 
-        public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
+        public Task<Tuple<MediaSourceInfo, IDirectStreamProvider, bool>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
         {
         {
-            return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
+            return GetLiveStream(id, mediaSourceId, true, cancellationToken);
         }
         }
 
 
         private string GetItemExternalId(BaseItem item)
         private string GetItemExternalId(BaseItem item)
@@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.LiveTv
             return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
             return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
-        private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
+        private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider, bool>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
         {
         {
             if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -319,6 +319,7 @@ namespace Emby.Server.Implementations.LiveTv
             bool isVideo;
             bool isVideo;
             ILiveTvService service;
             ILiveTvService service;
             IDirectStreamProvider directStreamProvider = null;
             IDirectStreamProvider directStreamProvider = null;
+            var assumeInterlaced = false;
 
 
             if (isChannel)
             if (isChannel)
             {
             {
@@ -365,10 +366,14 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 }
             }
             }
 
 
-            _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
             Normalize(info, service, isVideo);
             Normalize(info, service, isVideo);
 
 
-            return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider);
+            if (!(service is EmbyTV.EmbyTV))
+            {
+                assumeInterlaced = true;
+            }
+
+            return new Tuple<MediaSourceInfo, IDirectStreamProvider, bool>(info, directStreamProvider, assumeInterlaced);
         }
         }
 
 
         private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
         private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)

+ 4 - 1
Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs

@@ -126,12 +126,14 @@ namespace Emby.Server.Implementations.LiveTv
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
             var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
             var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
             IDirectStreamProvider directStreamProvider = null;
             IDirectStreamProvider directStreamProvider = null;
+            var assumeInterlaced = false;
 
 
             if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
             {
             {
                 var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
                 var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
                 stream = info.Item1;
                 stream = info.Item1;
                 directStreamProvider = info.Item2;
                 directStreamProvider = info.Item2;
+                assumeInterlaced = info.Item3;
             }
             }
             else
             else
             {
             {
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 }
                 else
                 else
                 {
                 {
-                    await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
+                    await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, assumeInterlaced, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -154,6 +156,7 @@ namespace Emby.Server.Implementations.LiveTv
                 _logger.ErrorException("Error probing live tv stream", ex);
                 _logger.ErrorException("Error probing live tv stream", ex);
             }
             }
 
 
+            _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(stream));
             return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
             return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
         }
         }
 
 

+ 9 - 6
Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs

@@ -50,13 +50,16 @@ namespace Emby.Server.Implementations.LiveTv
                 {
                 {
                     var channel = _liveTvManager.GetInternalChannel(liveTvItem.ChannelId);
                     var channel = _liveTvManager.GetInternalChannel(liveTvItem.ChannelId);
 
 
-                    var response = await service.GetProgramImageAsync(GetItemExternalId(liveTvItem), GetItemExternalId(channel), cancellationToken).ConfigureAwait(false);
-
-                    if (response != null)
+                    if (channel != null)
                     {
                     {
-                        imageResponse.HasImage = true;
-                        imageResponse.Stream = response.Stream;
-                        imageResponse.Format = response.Format;
+                        var response = await service.GetProgramImageAsync(GetItemExternalId(liveTvItem), GetItemExternalId(channel), cancellationToken).ConfigureAwait(false);
+
+                        if (response != null)
+                        {
+                            imageResponse.HasImage = true;
+                            imageResponse.Stream = response.Stream;
+                            imageResponse.Format = response.Format;
+                        }
                     }
                     }
                 }
                 }
                 catch (NotImplementedException)
                 catch (NotImplementedException)

+ 1 - 1
Emby.Server.Implementations/packages.config

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
 <packages>
-  <package id="Emby.XmlTv" version="1.0.5" targetFramework="portable45-net45+win8" />
+  <package id="Emby.XmlTv" version="1.0.6" targetFramework="portable45-net45+win8" />
   <package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
   <package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCLRaw.core" version="1.1.1" targetFramework="portable45-net45+win8" />
   <package id="SQLitePCLRaw.core" version="1.1.1" targetFramework="portable45-net45+win8" />

+ 1 - 1
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -791,7 +791,7 @@ namespace MediaBrowser.Api.LiveTv
                 ProviderChannels = providerChannels.Select(i => new NameIdPair
                 ProviderChannels = providerChannels.Select(i => new NameIdPair
                 {
                 {
                     Name = i.Name,
                     Name = i.Name,
-                    Id = i.TunerChannelId
+                    Id = string.IsNullOrWhiteSpace(i.TunerChannelId) ? i.Id : i.TunerChannelId
 
 
                 }).ToList(),
                 }).ToList(),
 
 

+ 1 - 0
MediaBrowser.Api/StartupWizardService.cs

@@ -119,6 +119,7 @@ namespace MediaBrowser.Api
             config.SkipDeserializationForAudio = true;
             config.SkipDeserializationForAudio = true;
             config.EnableSeriesPresentationUniqueKey = true;
             config.EnableSeriesPresentationUniqueKey = true;
             config.EnableLocalizedGuids = true;
             config.EnableLocalizedGuids = true;
+            config.EnableSimpleArtistDetection = true;
         }
         }
 
 
         public void Post(UpdateStartupConfiguration request)
         public void Post(UpdateStartupConfiguration request)

+ 1 - 0
MediaBrowser.Common/Net/HttpRequestOptions.cs

@@ -100,6 +100,7 @@ namespace MediaBrowser.Common.Net
 
 
         public int TimeoutMs { get; set; }
         public int TimeoutMs { get; set; }
         public bool PreferIpv4 { get; set; }
         public bool PreferIpv4 { get; set; }
+        public bool EnableDefaultUserAgent { get; set; }
 
 
         private string GetHeaderValue(string name)
         private string GetHeaderValue(string name)
         {
         {

+ 1 - 1
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -157,7 +157,7 @@ namespace MediaBrowser.Controller.LiveTv
         /// <param name="mediaSourceId">The media source identifier.</param>
         /// <param name="mediaSourceId">The media source identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{StreamResponseInfo}.</returns>
         /// <returns>Task{StreamResponseInfo}.</returns>
-        Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
+        Task<Tuple<MediaSourceInfo, IDirectStreamProvider, bool>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
 
 
         /// <summary>
         /// <summary>
         /// Gets the program.
         /// Gets the program.

+ 0 - 5
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -725,11 +725,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
 
 
             if (video.Protocol != MediaProtocol.File)
             if (video.Protocol != MediaProtocol.File)
             {
             {
-                // If it's mpeg based, assume true
-                if ((videoStream.Codec ?? string.Empty).IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)
-                {
-                    return true;
-                }
                 return false;
                 return false;
             }
             }
 
 

+ 12 - 0
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -192,6 +192,10 @@ namespace MediaBrowser.Model.Configuration
         public bool EnableExternalContentInSuggestions { get; set; }
         public bool EnableExternalContentInSuggestions { get; set; }
 
 
         public int ImageExtractionTimeoutMs { get; set; }
         public int ImageExtractionTimeoutMs { get; set; }
+
+        public PathSubstitution[] PathSubstitutions { get; set; }
+        public bool EnableSimpleArtistDetection { get; set; }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
         /// </summary>
@@ -202,6 +206,8 @@ namespace MediaBrowser.Model.Configuration
             Migrations = new string[] { };
             Migrations = new string[] { };
             ImageExtractionTimeoutMs = 0;
             ImageExtractionTimeoutMs = 0;
             EnableLocalizedGuids = true;
             EnableLocalizedGuids = true;
+            PathSubstitutions = new PathSubstitution[] { };
+            EnableSimpleArtistDetection = true;
 
 
             DisplaySpecialsWithinSeasons = true;
             DisplaySpecialsWithinSeasons = true;
             EnableExternalContentInSuggestions = true;
             EnableExternalContentInSuggestions = true;
@@ -563,4 +569,10 @@ namespace MediaBrowser.Model.Configuration
             };
             };
         }
         }
     }
     }
+
+    public class PathSubstitution
+    {
+        public string From { get; set; }
+        public string To { get; set; }
+    }
 }
 }

+ 0 - 4
MediaBrowser.Model/Entities/MetadataProviders.cs

@@ -24,10 +24,6 @@ namespace MediaBrowser.Model.Entities
         /// </summary>
         /// </summary>
         Tvcom = 5,
         Tvcom = 5,
         /// <summary>
         /// <summary>
-        /// The rotten tomatoes
-        /// </summary>
-        RottenTomatoes = 6,
-        /// <summary>
         /// Tmdb Collection Id
         /// Tmdb Collection Id
         /// </summary>
         /// </summary>
         TmdbCollection = 7,
         TmdbCollection = 7,

+ 2 - 1
MediaBrowser.Model/System/IEnvironmentInfo.cs

@@ -17,6 +17,7 @@ namespace MediaBrowser.Model.System
     {
     {
         Windows,
         Windows,
         Linux,
         Linux,
-        OSX
+        OSX,
+        BSD
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs

@@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.Music
 
 
             if (fileInfo.Exists)
             if (fileInfo.Exists)
             {
             {
-                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 7)
+                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3)
                 {
                 {
                     return _cachedTask;
                     return _cachedTask;
                 }
                 }

+ 11 - 6
MediaBrowser.Providers/Omdb/OmdbProvider.cs

@@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.Omdb
                 Url = url,
                 Url = url,
                 ResourcePool = ResourcePool,
                 ResourcePool = ResourcePool,
                 CancellationToken = cancellationToken,
                 CancellationToken = cancellationToken,
-                BufferContent = true
+                BufferContent = true,
+                EnableDefaultUserAgent = true
             });
             });
         }
         }
 
 
@@ -384,10 +385,11 @@ namespace MediaBrowser.Providers.Omdb
         {
         {
             T item = itemResult.Item;
             T item = itemResult.Item;
 
 
+            var isConfiguredForEnglish = IsConfiguredForEnglish(item);
+
             // Grab series genres because imdb data is better than tvdb. Leave movies alone
             // Grab series genres because imdb data is better than tvdb. Leave movies alone
             // But only do it if english is the preferred language because this data will not be localized
             // But only do it if english is the preferred language because this data will not be localized
-            if (ShouldFetchGenres(item) &&
-                !string.IsNullOrWhiteSpace(result.Genre))
+            if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre))
             {
             {
                 item.Genres.Clear();
                 item.Genres.Clear();
 
 
@@ -417,8 +419,11 @@ namespace MediaBrowser.Providers.Omdb
                 hasAwards.AwardSummary = WebUtility.HtmlDecode(result.Awards);
                 hasAwards.AwardSummary = WebUtility.HtmlDecode(result.Awards);
             }
             }
 
 
-            // Imdb plots are usually pretty short
-            item.Overview = result.Plot;
+            if (isConfiguredForEnglish)
+            {
+                // Omdb is currently english only, so for other languages skip this and let secondary providers fill it in
+                item.Overview = result.Plot;
+            }
 
 
             //if (!string.IsNullOrWhiteSpace(result.Director))
             //if (!string.IsNullOrWhiteSpace(result.Director))
             //{
             //{
@@ -461,7 +466,7 @@ namespace MediaBrowser.Providers.Omdb
             //}
             //}
         }
         }
 
 
-        private bool ShouldFetchGenres(BaseItem item)
+        private bool IsConfiguredForEnglish(BaseItem item)
         {
         {
             var lang = item.GetPreferredMetadataLanguage();
             var lang = item.GetPreferredMetadataLanguage();
 
 

+ 13 - 0
MediaBrowser.Server.Mono/Program.cs

@@ -319,5 +319,18 @@ namespace MediaBrowser.Server.Mono
         {
         {
             return Syscall.getuid().ToString(CultureInfo.InvariantCulture);
             return Syscall.getuid().ToString(CultureInfo.InvariantCulture);
         }
         }
+
+        public override Model.System.OperatingSystem OperatingSystem
+        {
+            get
+            {
+                if (IsBsd)
+                {
+                    return Model.System.OperatingSystem.BSD;
+                }
+
+                return base.OperatingSystem;
+            }
+        }
     }
     }
 }
 }

+ 56 - 51
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -139,6 +139,23 @@ namespace MediaBrowser.WebDashboard.Api
             _memoryStreamFactory = memoryStreamFactory;
             _memoryStreamFactory = memoryStreamFactory;
         }
         }
 
 
+        /// <summary>
+        /// Gets the dashboard UI path.
+        /// </summary>
+        /// <value>The dashboard UI path.</value>
+        public string DashboardUIPath
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath))
+                {
+                    return _serverConfigurationManager.Configuration.DashboardSourcePath;
+                }
+
+                return Path.Combine(_serverConfigurationManager.ApplicationPaths.ApplicationResourcesPath, "dashboard-ui");
+            }
+        }
+
         public object Get(GetFavIcon request)
         public object Get(GetFavIcon request)
         {
         {
             return Get(new GetDashboardResource
             return Get(new GetDashboardResource
@@ -176,7 +193,7 @@ namespace MediaBrowser.WebDashboard.Api
 
 
             if (plugin != null && stream != null)
             if (plugin != null && stream != null)
             {
             {
-                return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null));
+                return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null));
             }
             }
 
 
             throw new ResourceNotFoundException();
             throw new ResourceNotFoundException();
@@ -274,9 +291,11 @@ namespace MediaBrowser.WebDashboard.Api
             path = path.Replace("bower_components" + _appHost.ApplicationVersion, "bower_components", StringComparison.OrdinalIgnoreCase);
             path = path.Replace("bower_components" + _appHost.ApplicationVersion, "bower_components", StringComparison.OrdinalIgnoreCase);
 
 
             var contentType = MimeTypes.GetMimeType(path);
             var contentType = MimeTypes.GetMimeType(path);
+            var basePath = DashboardUIPath;
 
 
             // Bounce them to the startup wizard if it hasn't been completed yet
             // Bounce them to the startup wizard if it hasn't been completed yet
-            if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted && path.IndexOf("wizard", StringComparison.OrdinalIgnoreCase) == -1 && GetPackageCreator().IsCoreHtml(path))
+            if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted &&
+                path.IndexOf("wizard", StringComparison.OrdinalIgnoreCase) == -1 && GetPackageCreator(basePath).IsCoreHtml(path))
             {
             {
                 // But don't redirect if an html import is being requested.
                 // But don't redirect if an html import is being requested.
                 if (path.IndexOf("bower_components", StringComparison.OrdinalIgnoreCase) == -1)
                 if (path.IndexOf("bower_components", StringComparison.OrdinalIgnoreCase) == -1)
@@ -296,7 +315,7 @@ namespace MediaBrowser.WebDashboard.Api
                 !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
                 !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
                 !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
                 !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase))
             {
             {
-                var stream = await GetResourceStream(path, localizationCulture).ConfigureAwait(false);
+                var stream = await GetResourceStream(basePath, path, localizationCulture).ConfigureAwait(false);
                 return _resultFactory.GetResult(stream, contentType);
                 return _resultFactory.GetResult(stream, contentType);
             }
             }
 
 
@@ -311,7 +330,7 @@ namespace MediaBrowser.WebDashboard.Api
 
 
             var cacheKey = (_appHost.ApplicationVersion + (localizationCulture ?? string.Empty) + path).GetMD5();
             var cacheKey = (_appHost.ApplicationVersion + (localizationCulture ?? string.Empty) + path).GetMD5();
 
 
-            return await _resultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, localizationCulture)).ConfigureAwait(false);
+            return await _resultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(basePath, path, localizationCulture)).ConfigureAwait(false);
         }
         }
 
 
         private string GetLocalizationCulture()
         private string GetLocalizationCulture()
@@ -322,86 +341,72 @@ namespace MediaBrowser.WebDashboard.Api
         /// <summary>
         /// <summary>
         /// Gets the resource stream.
         /// Gets the resource stream.
         /// </summary>
         /// </summary>
-        /// <param name="path">The path.</param>
-        /// <param name="localizationCulture">The localization culture.</param>
-        /// <returns>Task{Stream}.</returns>
-        private Task<Stream> GetResourceStream(string path, string localizationCulture)
+        private Task<Stream> GetResourceStream(string basePath, string virtualPath, string localizationCulture)
         {
         {
-            return GetPackageCreator()
-                .GetResource(path, null, localizationCulture, _appHost.ApplicationVersion.ToString());
+            return GetPackageCreator(basePath)
+                .GetResource(virtualPath, null, localizationCulture, _appHost.ApplicationVersion.ToString());
         }
         }
 
 
-        private PackageCreator GetPackageCreator()
+        private PackageCreator GetPackageCreator(string basePath)
         {
         {
-            return new PackageCreator(_fileSystem, _logger, _serverConfigurationManager, _memoryStreamFactory);
+            return new PackageCreator(basePath, _fileSystem, _logger, _serverConfigurationManager, _memoryStreamFactory);
         }
         }
 
 
         public async Task<object> Get(GetDashboardPackage request)
         public async Task<object> Get(GetDashboardPackage request)
         {
         {
             var mode = request.Mode;
             var mode = request.Mode;
 
 
-            var path = !string.IsNullOrWhiteSpace(mode) ?
-                Path.Combine(_serverConfigurationManager.ApplicationPaths.ProgramDataPath, "webclient-dump")
+            var inputPath = string.IsNullOrWhiteSpace(mode) ?
+                DashboardUIPath
+                : "C:\\dev\\emby-web-mobile-master\\dist";
+
+            var targetPath = !string.IsNullOrWhiteSpace(mode) ?
+                inputPath
                 : "C:\\dev\\emby-web-mobile\\src";
                 : "C:\\dev\\emby-web-mobile\\src";
 
 
-            try
-            {
-                _fileSystem.DeleteDirectory(path, true);
-            }
-            catch (IOException)
-            {
+            var packageCreator = GetPackageCreator(inputPath);
 
 
-            }
+            if (!string.Equals(inputPath, targetPath, StringComparison.OrdinalIgnoreCase))
+            {
+                try
+                {
+                    _fileSystem.DeleteDirectory(targetPath, true);
+                }
+                catch (IOException)
+                {
 
 
-            var creator = GetPackageCreator();
+                }
 
 
-            CopyDirectory(creator.DashboardUIPath, path);
+                CopyDirectory(inputPath, targetPath);
+            }
 
 
             string culture = null;
             string culture = null;
 
 
             var appVersion = _appHost.ApplicationVersion.ToString();
             var appVersion = _appHost.ApplicationVersion.ToString();
 
 
-            // Try to trim the output size a bit
-            var bowerPath = Path.Combine(path, "bower_components");
-
-            if (!string.IsNullOrWhiteSpace(mode))
-            {
-                // Delete things that are unneeded in an attempt to keep the output as trim as possible
-
-                DeleteFoldersByName(Path.Combine(bowerPath, "emby-webcomponents", "fonts"), "roboto");
-                _fileSystem.DeleteDirectory(Path.Combine(path, "css", "images", "tour"), true);
-            }
-
-            await DumpHtml(creator.DashboardUIPath, path, mode, culture, appVersion);
+            await DumpHtml(packageCreator, inputPath, targetPath, mode, culture, appVersion);
 
 
             return "";
             return "";
         }
         }
 
 
-        private void DeleteFoldersByName(string path, string name)
-        {
-            var directories = _fileSystem.GetDirectories(path, true)
-                .Where(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase))
-                .ToList();
-
-            foreach (var directory in directories)
-            {
-                _fileSystem.DeleteDirectory(directory.FullName, true);
-            }
-        }
-
-        private async Task DumpHtml(string source, string destination, string mode, string culture, string appVersion)
+        private async Task DumpHtml(PackageCreator packageCreator, string source, string destination, string mode, string culture, string appVersion)
         {
         {
             foreach (var file in _fileSystem.GetFiles(source))
             foreach (var file in _fileSystem.GetFiles(source))
             {
             {
                 var filename = file.Name;
                 var filename = file.Name;
 
 
-                await DumpFile(filename, Path.Combine(destination, filename), mode, culture, appVersion).ConfigureAwait(false);
+                if (!string.Equals(file.Extension, ".html", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
+                await DumpFile(packageCreator, filename, Path.Combine(destination, filename), mode, culture, appVersion).ConfigureAwait(false);
             }
             }
         }
         }
 
 
-        private async Task DumpFile(string resourceVirtualPath, string destinationFilePath, string mode, string culture, string appVersion)
+        private async Task DumpFile(PackageCreator packageCreator, string resourceVirtualPath, string destinationFilePath, string mode, string culture, string appVersion)
         {
         {
-            using (var stream = await GetPackageCreator().GetResource(resourceVirtualPath, mode, culture, appVersion).ConfigureAwait(false))
+            using (var stream = await packageCreator.GetResource(resourceVirtualPath, mode, culture, appVersion).ConfigureAwait(false))
             {
             {
                 using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
                 using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read))
                 {
                 {

+ 14 - 36
MediaBrowser.WebDashboard/Api/PackageCreator.cs

@@ -19,31 +19,33 @@ namespace MediaBrowser.WebDashboard.Api
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IMemoryStreamFactory _memoryStreamFactory;
         private readonly IMemoryStreamFactory _memoryStreamFactory;
+        private readonly string _basePath;
 
 
-        public PackageCreator(IFileSystem fileSystem, ILogger logger, IServerConfigurationManager config, IMemoryStreamFactory memoryStreamFactory)
+        public PackageCreator(string basePath, IFileSystem fileSystem, ILogger logger, IServerConfigurationManager config, IMemoryStreamFactory memoryStreamFactory)
         {
         {
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _logger = logger;
             _logger = logger;
             _config = config;
             _config = config;
             _memoryStreamFactory = memoryStreamFactory;
             _memoryStreamFactory = memoryStreamFactory;
+            _basePath = basePath;
         }
         }
 
 
-        public async Task<Stream> GetResource(string path,
+        public async Task<Stream> GetResource(string virtualPath,
             string mode,
             string mode,
             string localizationCulture,
             string localizationCulture,
             string appVersion)
             string appVersion)
         {
         {
-            var resourceStream = GetRawResourceStream(path);
+            var resourceStream = GetRawResourceStream(virtualPath);
 
 
             if (resourceStream != null)
             if (resourceStream != null)
             {
             {
                 // Don't apply any caching for html pages
                 // Don't apply any caching for html pages
                 // jQuery ajax doesn't seem to handle if-modified-since correctly
                 // jQuery ajax doesn't seem to handle if-modified-since correctly
-                if (IsFormat(path, "html"))
+                if (IsFormat(virtualPath, "html"))
                 {
                 {
-                    if (IsCoreHtml(path))
+                    if (IsCoreHtml(virtualPath))
                     {
                     {
-                        resourceStream = await ModifyHtml(path, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false);
+                        resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false);
                     }
                     }
                 }
                 }
             }
             }
@@ -62,33 +64,13 @@ namespace MediaBrowser.WebDashboard.Api
             return Path.GetExtension(path).EndsWith(format, StringComparison.OrdinalIgnoreCase);
             return Path.GetExtension(path).EndsWith(format, StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
-        /// <summary>
-        /// Gets the dashboard UI path.
-        /// </summary>
-        /// <value>The dashboard UI path.</value>
-        public string DashboardUIPath
-        {
-            get
-            {
-                if (!string.IsNullOrEmpty(_config.Configuration.DashboardSourcePath))
-                {
-                    return _config.Configuration.DashboardSourcePath;
-                }
-
-                return Path.Combine(_config.ApplicationPaths.ApplicationResourcesPath, "dashboard-ui");
-            }
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the dashboard resource path.
         /// Gets the dashboard resource path.
         /// </summary>
         /// </summary>
-        /// <param name="virtualPath">The virtual path.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
         private string GetDashboardResourcePath(string virtualPath)
         private string GetDashboardResourcePath(string virtualPath)
         {
         {
-            var rootPath = DashboardUIPath;
-
-            var fullPath = Path.Combine(rootPath, virtualPath.Replace('/', _fileSystem.DirectorySeparatorChar));
+            var fullPath = Path.Combine(_basePath, virtualPath.Replace('/', _fileSystem.DirectorySeparatorChar));
 
 
             try
             try
             {
             {
@@ -100,7 +82,7 @@ namespace MediaBrowser.WebDashboard.Api
             }
             }
 
 
             // Don't allow file system access outside of the source folder
             // Don't allow file system access outside of the source folder
-            if (!_fileSystem.ContainsSubPath(rootPath, fullPath))
+            if (!_fileSystem.ContainsSubPath(_basePath, fullPath))
             {
             {
                 throw new SecurityException("Access denied");
                 throw new SecurityException("Access denied");
             }
             }
@@ -118,10 +100,8 @@ namespace MediaBrowser.WebDashboard.Api
             path = GetDashboardResourcePath(path);
             path = GetDashboardResourcePath(path);
             var parent = Path.GetDirectoryName(path);
             var parent = Path.GetDirectoryName(path);
 
 
-            var basePath = DashboardUIPath;
-
-            return string.Equals(basePath, parent, StringComparison.OrdinalIgnoreCase) ||
-                   string.Equals(Path.Combine(basePath, "voice"), parent, StringComparison.OrdinalIgnoreCase);
+            return string.Equals(_basePath, parent, StringComparison.OrdinalIgnoreCase) ||
+                   string.Equals(Path.Combine(_basePath, "voice"), parent, StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -319,11 +299,9 @@ namespace MediaBrowser.WebDashboard.Api
         /// <summary>
         /// <summary>
         /// Gets the raw resource stream.
         /// Gets the raw resource stream.
         /// </summary>
         /// </summary>
-        /// <param name="path">The path.</param>
-        /// <returns>Task{Stream}.</returns>
-        private Stream GetRawResourceStream(string path)
+        private Stream GetRawResourceStream(string virtualPath)
         {
         {
-            return _fileSystem.GetFileStream(GetDashboardResourcePath(path), FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true);
+            return _fileSystem.GetFileStream(GetDashboardResourcePath(virtualPath), FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true);
         }
         }
 
 
     }
     }

+ 0 - 8
MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

@@ -579,14 +579,6 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 writer.WriteElementString("website", item.HomePageUrl);
                 writer.WriteElementString("website", item.HomePageUrl);
             }
             }
 
 
-            var rt = item.GetProviderId(MetadataProviders.RottenTomatoes);
-
-            if (!string.IsNullOrEmpty(rt))
-            {
-                writer.WriteElementString("rottentomatoesid", rt);
-                writtenProviderIds.Add(MetadataProviders.RottenTomatoes.ToString());
-            }
-
             var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
             var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
 
 
             if (!string.IsNullOrEmpty(tmdbCollection))
             if (!string.IsNullOrEmpty(tmdbCollection))

+ 51 - 122
RSSDP/SsdpDeviceLocatorBase.cs

@@ -24,11 +24,9 @@ namespace Rssdp.Infrastructure
         private List<DiscoveredSsdpDevice> _Devices;
         private List<DiscoveredSsdpDevice> _Devices;
         private ISsdpCommunicationsServer _CommunicationsServer;
         private ISsdpCommunicationsServer _CommunicationsServer;
 
 
-        private IList<DiscoveredSsdpDevice> _SearchResults;
-        private object _SearchResultsSynchroniser;
-
-        private ITimer _ExpireCachedDevicesTimer;
+        private ITimer _BroadcastTimer;
         private ITimerFactory _timerFactory;
         private ITimerFactory _timerFactory;
+        private object _timerLock = new object();
 
 
         private static readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4);
         private static readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4);
         private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1);
         private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1);
@@ -48,7 +46,6 @@ namespace Rssdp.Infrastructure
             _timerFactory = timerFactory;
             _timerFactory = timerFactory;
             _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived;
             _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived;
 
 
-            _SearchResultsSynchroniser = new object();
             _Devices = new List<DiscoveredSsdpDevice>();
             _Devices = new List<DiscoveredSsdpDevice>();
         }
         }
 
 
@@ -92,11 +89,52 @@ namespace Rssdp.Infrastructure
 
 
         #region Search Overloads
         #region Search Overloads
 
 
+        public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period)
+        {
+            lock (_timerLock)
+            {
+                if (_BroadcastTimer == null)
+                {
+                    _BroadcastTimer = _timerFactory.Create(OnBroadcastTimerCallback, null, dueTime, period);
+                }
+                else
+                {
+                    _BroadcastTimer.Change(dueTime, period);
+                }
+            }
+        }
+
+        public void DisposeBroadcastTimer()
+        {
+            lock (_timerLock)
+            {
+                if (_BroadcastTimer != null)
+                {
+                    _BroadcastTimer.Dispose();
+                    _BroadcastTimer = null;
+                }
+            }
+        }
+
+        private async void OnBroadcastTimerCallback(object state)
+        {
+            StartListeningForNotifications();
+            RemoveExpiredDevicesFromCache();
+
+            try
+            {
+                await SearchAsync(CancellationToken.None).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Performs a search for all devices using the default search timeout.
         /// Performs a search for all devices using the default search timeout.
         /// </summary>
         /// </summary>
-        /// <returns>A task whose result is an <see cref="IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
-        public Task<IEnumerable<DiscoveredSsdpDevice>> SearchAsync(CancellationToken cancellationToken)
+        private Task SearchAsync(CancellationToken cancellationToken)
         {
         {
             return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken);
             return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken);
         }
         }
@@ -111,8 +149,7 @@ namespace Rssdp.Infrastructure
         /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
         /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
         /// </list>
         /// </list>
         /// </param>
         /// </param>
-        /// <returns>A task whose result is an <see cref="IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
-        public Task<IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget)
+        private Task SearchAsync(string searchTarget)
         {
         {
             return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None);
             return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None);
         }
         }
@@ -121,13 +158,12 @@ namespace Rssdp.Infrastructure
         /// Performs a search for all devices using the specified search timeout.
         /// Performs a search for all devices using the specified search timeout.
         /// </summary>
         /// </summary>
         /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param>
         /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param>
-        /// <returns>A task whose result is an <see cref="IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
-        public Task<IEnumerable<DiscoveredSsdpDevice>> SearchAsync(TimeSpan searchWaitTime)
+        private Task SearchAsync(TimeSpan searchWaitTime)
         {
         {
             return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None);
             return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None);
         }
         }
 
 
-        public async Task<IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken)
+        private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken)
         {
         {
             if (searchTarget == null) throw new ArgumentNullException("searchTarget");
             if (searchTarget == null) throw new ArgumentNullException("searchTarget");
             if (searchTarget.Length == 0) throw new ArgumentException("searchTarget cannot be an empty string.", "searchTarget");
             if (searchTarget.Length == 0) throw new ArgumentException("searchTarget cannot be an empty string.", "searchTarget");
@@ -136,48 +172,7 @@ namespace Rssdp.Infrastructure
 
 
             ThrowIfDisposed();
             ThrowIfDisposed();
 
 
-            if (_SearchResults != null) throw new InvalidOperationException("Search already in progress. Only one search at a time is allowed.");
-            _SearchResults = new List<DiscoveredSsdpDevice>();
-
-            // If searchWaitTime == 0 then we are only going to report unexpired cached items, not actually do a search.
-            if (searchWaitTime > TimeSpan.Zero)
-                await BroadcastDiscoverMessage(searchTarget, SearchTimeToMXValue(searchWaitTime), cancellationToken).ConfigureAwait(false);
-
-            lock (_SearchResultsSynchroniser)
-            {
-                foreach (var device in GetUnexpiredDevices().Where(NotificationTypeMatchesFilter))
-                {
-                    DeviceFound(device, false, null);
-                }
-            }
-
-            if (searchWaitTime != TimeSpan.Zero)
-                await Task.Delay(searchWaitTime, cancellationToken).ConfigureAwait(false);
-
-            IEnumerable<DiscoveredSsdpDevice> retVal = null;
-
-            try
-            {
-                lock (_SearchResultsSynchroniser)
-                {
-                    retVal = _SearchResults;
-                    _SearchResults = null;
-                }
-
-                RemoveExpiredDevicesFromCache();
-            }
-            finally
-            {
-                var server = _CommunicationsServer;
-                try
-                {
-                    if (server != null) // In case we were disposed while searching.
-                        server.StopListeningForResponses();
-                }
-                catch (ObjectDisposedException) { }
-            }
-
-            return retVal;
+            return BroadcastDiscoverMessage(searchTarget, SearchTimeToMXValue(searchWaitTime), cancellationToken);
         }
         }
 
 
         #endregion
         #endregion
@@ -287,9 +282,7 @@ namespace Rssdp.Infrastructure
 
 
             if (disposing)
             if (disposing)
             {
             {
-                var timer = _ExpireCachedDevicesTimer;
-                if (timer != null)
-                    timer.Dispose();
+                DisposeBroadcastTimer();
 
 
                 var commsServer = _CommunicationsServer;
                 var commsServer = _CommunicationsServer;
                 _CommunicationsServer = null;
                 _CommunicationsServer = null;
@@ -332,40 +325,9 @@ namespace Rssdp.Infrastructure
 
 
         private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress)
         private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress)
         {
         {
-            // Don't raise the event if we've already done it for a cached
-            // version of this device, and the cached version isn't
-            // "significantly" different, i.e location and cachelifetime 
-            // haven't changed.
-            var raiseEvent = false;
-
             if (!NotificationTypeMatchesFilter(device)) return;
             if (!NotificationTypeMatchesFilter(device)) return;
 
 
-            lock (_SearchResultsSynchroniser)
-            {
-                if (_SearchResults != null)
-                {
-                    var existingDevice = FindExistingDeviceNotification(_SearchResults, device.NotificationType, device.Usn);
-                    if (existingDevice == null)
-                    {
-                        _SearchResults.Add(device);
-                        raiseEvent = true;
-                    }
-                    else
-                    {
-                        if (existingDevice.DescriptionLocation != device.DescriptionLocation || existingDevice.CacheLifetime != device.CacheLifetime)
-                        {
-                            _SearchResults.Remove(existingDevice);
-                            _SearchResults.Add(device);
-                            raiseEvent = true;
-                        }
-                    }
-                }
-                else
-                    raiseEvent = true;
-            }
-
-            if (raiseEvent)
-                OnDeviceAvailable(device, isNewDevice, localIpAddress);
+            OnDeviceAvailable(device, isNewDevice, localIpAddress);
         }
         }
 
 
         private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
         private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
@@ -450,8 +412,6 @@ namespace Rssdp.Infrastructure
                 };
                 };
 
 
                 AddOrUpdateDiscoveredDevice(device, localIpAddress);
                 AddOrUpdateDiscoveredDevice(device, localIpAddress);
-
-                ResetExpireCachedDevicesTimer();
             }
             }
         }
         }
 
 
@@ -477,26 +437,9 @@ namespace Rssdp.Infrastructure
                     if (NotificationTypeMatchesFilter(deadDevice))
                     if (NotificationTypeMatchesFilter(deadDevice))
                         OnDeviceUnavailable(deadDevice, false);
                         OnDeviceUnavailable(deadDevice, false);
                 }
                 }
-
-                ResetExpireCachedDevicesTimer();
             }
             }
         }
         }
 
 
-        private void ResetExpireCachedDevicesTimer()
-        {
-            if (IsDisposed) return;
-
-            if (_ExpireCachedDevicesTimer == null)
-                _ExpireCachedDevicesTimer = _timerFactory.Create(this.ExpireCachedDevices, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
-
-            _ExpireCachedDevicesTimer.Change(60000, System.Threading.Timeout.Infinite);
-        }
-
-        private void ExpireCachedDevices(object state)
-        {
-            RemoveExpiredDevicesFromCache();
-        }
-
         #region Header/Message Processing Utilities
         #region Header/Message Processing Utilities
 
 
         private static string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
         private static string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
@@ -624,20 +567,6 @@ namespace Rssdp.Infrastructure
 
 
             if (existingDevices != null && existingDevices.Any())
             if (existingDevices != null && existingDevices.Any())
             {
             {
-                lock (_SearchResultsSynchroniser)
-                {
-                    if (_SearchResults != null)
-                    {
-                        var resultsToRemove = (from result in _SearchResults where result.Usn == deviceUsn select result).ToArray();
-                        foreach (var result in resultsToRemove)
-                        {
-                            if (this.IsDisposed) return true;
-
-                            _SearchResults.Remove(result);
-                        }
-                    }
-                }
-
                 foreach (var removedDevice in existingDevices)
                 foreach (var removedDevice in existingDevices)
                 {
                 {
                     if (NotificationTypeMatchesFilter(removedDevice))
                     if (NotificationTypeMatchesFilter(removedDevice))