فهرست منبع

Merge pull request #1873 from MediaBrowser/dev

Dev
Luke 9 سال پیش
والد
کامیت
cc9145b398
27فایلهای تغییر یافته به همراه429 افزوده شده و 111 حذف شده
  1. 19 1
      MediaBrowser.Api/ConfigurationService.cs
  2. 15 4
      MediaBrowser.Api/ItemLookupService.cs
  3. 7 2
      MediaBrowser.Api/StartupWizardService.cs
  4. 3 3
      MediaBrowser.Api/System/SystemService.cs
  5. 11 5
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  6. 2 0
      MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
  7. 58 14
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  8. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  9. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  10. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  11. 9 0
      MediaBrowser.Model/System/Architecture.cs
  12. 2 0
      MediaBrowser.Model/System/SystemInfo.cs
  13. 19 5
      MediaBrowser.Providers/Movies/MovieDbSearch.cs
  14. 9 1
      MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs
  15. 1 1
      MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs
  16. 200 0
      MediaBrowser.Providers/Omdb/OmdbProvider.cs
  17. 4 18
      MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs
  18. 4 0
      MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs
  19. 3 3
      MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  20. 20 0
      MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  21. 2 1
      MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
  22. 5 4
      MediaBrowser.Server.Mono/Native/BaseMonoApp.cs
  23. 10 4
      MediaBrowser.Server.Startup.Common/ApplicationHost.cs
  24. 5 13
      MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs
  25. 2 8
      MediaBrowser.Server.Startup.Common/NativeEnvironment.cs
  26. 3 24
      MediaBrowser.ServerApplication/Native/WindowsApp.cs
  27. 9 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

+ 19 - 1
MediaBrowser.Api/ConfigurationService.cs

@@ -9,7 +9,9 @@ using ServiceStack.Web;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using System.Threading.Tasks;
 using CommonIO;
+using MediaBrowser.Controller.MediaEncoding;
 
 namespace MediaBrowser.Api
 {
@@ -71,6 +73,14 @@ namespace MediaBrowser.Api
 
     }
 
+    [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")]
+    [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)]
+    public class UpdateMediaEncoderPath : IReturnVoid
+    {
+        [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Path { get; set; }
+    }
+
     public class ConfigurationService : BaseApiService
     {
         /// <summary>
@@ -86,14 +96,22 @@ namespace MediaBrowser.Api
         private readonly IFileSystem _fileSystem;
         private readonly IProviderManager _providerManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IMediaEncoder _mediaEncoder;
 
-        public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager)
+        public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager, IMediaEncoder mediaEncoder)
         {
             _jsonSerializer = jsonSerializer;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
             _providerManager = providerManager;
             _libraryManager = libraryManager;
+            _mediaEncoder = mediaEncoder;
+        }
+
+        public void Post(UpdateMediaEncoderPath request)
+        {
+            var task = _mediaEncoder.UpdateEncoderPath(request.Path);
+            Task.WaitAll(task);
         }
 
         /// <summary>

+ 15 - 4
MediaBrowser.Api/ItemLookupService.cs

@@ -38,6 +38,12 @@ namespace MediaBrowser.Api
     {
     }
 
+    [Route("/Items/RemoteSearch/Trailer", "POST")]
+    [Authenticated]
+    public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>>
+    {
+    }
+
     [Route("/Items/RemoteSearch/AdultVideo", "POST")]
     [Authenticated]
     public class GetAdultVideoRemoteSearchResults : RemoteSearchQuery<ItemLookupInfo>, IReturn<List<RemoteSearchResult>>
@@ -132,6 +138,13 @@ namespace MediaBrowser.Api
             return ToOptimizedResult(infos);
         }
 
+        public async Task<object> Post(GetTrailerRemoteSearchResults request)
+        {
+            var result = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(request, CancellationToken.None).ConfigureAwait(false);
+
+            return ToOptimizedResult(result);
+        }
+
         public async Task<object> Post(GetMovieRemoteSearchResults request)
         {
             var result = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).ConfigureAwait(false);
@@ -181,11 +194,9 @@ namespace MediaBrowser.Api
             return ToOptimizedResult(result);
         }
 
-        public async Task<object> Get(GetRemoteSearchImage request)
+        public Task<object> Get(GetRemoteSearchImage request)
         {
-            var result = GetRemoteImage(request).ConfigureAwait(false);
-
-            return result;
+            return GetRemoteImage(request);
         }
 
         public void Post(ApplySearchCriteria request)

+ 7 - 2
MediaBrowser.Api/StartupWizardService.cs

@@ -11,6 +11,7 @@ using ServiceStack;
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using MediaBrowser.Controller.MediaEncoding;
 
 namespace MediaBrowser.Api
 {
@@ -52,14 +53,16 @@ namespace MediaBrowser.Api
         private readonly IUserManager _userManager;
         private readonly IConnectManager _connectManager;
         private readonly ILiveTvManager _liveTvManager;
+        private readonly IMediaEncoder _mediaEncoder;
 
-        public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager)
+        public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager, IMediaEncoder mediaEncoder)
         {
             _config = config;
             _appHost = appHost;
             _userManager = userManager;
             _connectManager = connectManager;
             _liveTvManager = liveTvManager;
+            _mediaEncoder = mediaEncoder;
         }
 
         public void Post(ReportStartupWizardComplete request)
@@ -75,7 +78,8 @@ namespace MediaBrowser.Api
 
             return new StartupInfo
             {
-                SupportsRunningAsService = info.SupportsRunningAsService
+                SupportsRunningAsService = info.SupportsRunningAsService,
+                HasMediaEncoder = !string.IsNullOrWhiteSpace(_mediaEncoder.EncoderPath)
             };
         }
 
@@ -231,6 +235,7 @@ namespace MediaBrowser.Api
     public class StartupInfo
     {
         public bool SupportsRunningAsService { get; set; }
+        public bool HasMediaEncoder { get; set; }
     }
 
     public class StartupUser

+ 3 - 3
MediaBrowser.Api/System/SystemService.cs

@@ -19,7 +19,7 @@ namespace MediaBrowser.Api.System
     /// Class GetSystemInfo
     /// </summary>
     [Route("/System/Info", "GET", Summary = "Gets information about the server")]
-    [Authenticated(EscapeParentalControl = true)]
+    [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)]
     public class GetSystemInfo : IReturn<SystemInfo>
     {
 
@@ -120,7 +120,7 @@ namespace MediaBrowser.Api.System
 
             try
             {
-				files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
+                files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
                     .Where(i => string.Equals(i.Extension, ".txt", StringComparison.OrdinalIgnoreCase))
                     .ToList();
             }
@@ -146,7 +146,7 @@ namespace MediaBrowser.Api.System
 
         public Task<object> Get(GetLogFile request)
         {
-			var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
+            var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
                 .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
 
             return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);

+ 11 - 5
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -119,11 +119,17 @@ namespace MediaBrowser.Api.UserLibrary
 
             // Default list type = children
 
+            var folder = item as Folder;
+            if (folder == null)
+            {
+                folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder();
+            }
+
             if (!string.IsNullOrEmpty(request.Ids))
             {
                 request.Recursive = true;
                 var query = GetItemsQuery(request, user);
-                var result = await ((Folder)item).GetItems(query).ConfigureAwait(false);
+                var result = await folder.GetItems(query).ConfigureAwait(false);
 
                 if (string.IsNullOrWhiteSpace(request.SortBy))
                 {
@@ -138,22 +144,22 @@ namespace MediaBrowser.Api.UserLibrary
 
             if (request.Recursive)
             {
-                return await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
+                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
             }
 
             if (user == null)
             {
-                return await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
+                return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false);
             }
 
             var userRoot = item as UserRootFolder;
 
             if (userRoot == null)
             {
-                return await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
+                return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false);
             }
 
-            IEnumerable<BaseItem> items = ((Folder)item).GetChildren(user, true);
+            IEnumerable<BaseItem> items = folder.GetChildren(user, true);
 
             var itemsArray = items.ToArray();
 

+ 2 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -130,5 +130,7 @@ namespace MediaBrowser.Controller.MediaEncoding
         string EscapeSubtitleFilterPath(string path);
 
         void Init();
+
+        Task UpdateEncoderPath(string path);
     }
 }

+ 58 - 14
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -23,6 +23,7 @@ using System.Threading.Tasks;
 using CommonIO;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
 
 namespace MediaBrowser.MediaEncoding.Encoder
 {
@@ -118,6 +119,35 @@ namespace MediaBrowser.MediaEncoding.Encoder
             }
         }
 
+        public async Task UpdateEncoderPath(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                throw new ArgumentNullException("path");
+            }
+
+            if (!File.Exists(path) && !Directory.Exists(path))
+            {
+                throw new ResourceNotFoundException();
+            }
+
+            var newPaths = GetEncoderPaths(path);
+            if (string.IsNullOrWhiteSpace(newPaths.Item1))
+            {
+                throw new ResourceNotFoundException("ffmpeg not found");
+            }
+            if (string.IsNullOrWhiteSpace(newPaths.Item2))
+            {
+                throw new ResourceNotFoundException("ffprobe not found");
+            }
+
+            var config = GetEncodingOptions();
+            config.EncoderAppPath = path;
+            ConfigurationManager.SaveConfiguration("encoding", config);
+
+            Init();
+        }
+
         private void ConfigureEncoderPaths()
         {
             if (_hasExternalEncoder)
@@ -131,46 +161,60 @@ namespace MediaBrowser.MediaEncoding.Encoder
             {
                 appPath = Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "ffmpeg");
             }
+            var newPaths = GetEncoderPaths(appPath);
+
+            if (!string.IsNullOrWhiteSpace(newPaths.Item1) && !string.IsNullOrWhiteSpace(newPaths.Item2))
+            {
+                FFMpegPath = newPaths.Item1;
+                FFProbePath = newPaths.Item2;
+            }
+
+            LogPaths();
+        }
+
+        private Tuple<string, string> GetEncoderPaths(string configuredPath)
+        {
+            var appPath = configuredPath;
 
             if (!string.IsNullOrWhiteSpace(appPath))
             {
                 if (Directory.Exists(appPath))
                 {
-                    SetPathsFromDirectory(appPath);
+                    return GetPathsFromDirectory(appPath);
                 }
 
-                else if (File.Exists(appPath))
+                if (File.Exists(appPath))
                 {
-                    FFMpegPath = appPath;
-
-                    SetProbePathFromEncoderPath(appPath);
+                    return new Tuple<string, string>(appPath, GetProbePathFromEncoderPath(appPath));
                 }
             }
 
-            LogPaths();
+            return new Tuple<string, string>(null, null);
         }
 
-        private void SetPathsFromDirectory(string path)
+        private Tuple<string,string> GetPathsFromDirectory(string path)
         {
             // Since we can't predict the file extension, first try directly within the folder 
             // If that doesn't pan out, then do a recursive search
             var files = Directory.GetFiles(path);
 
-            FFMpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
-            FFProbePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
+            var ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
+            var ffprobePath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
 
-            if (string.IsNullOrWhiteSpace(FFMpegPath) || !File.Exists(FFMpegPath))
+            if (string.IsNullOrWhiteSpace(ffmpegPath) || !File.Exists(ffmpegPath))
             {
                 files = Directory.GetFiles(path, "*", SearchOption.AllDirectories);
 
-                FFMpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
-                SetProbePathFromEncoderPath(FFMpegPath);
+                ffmpegPath = files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffmpeg", StringComparison.OrdinalIgnoreCase));
+                ffprobePath = GetProbePathFromEncoderPath(ffmpegPath);
             }
+
+            return new Tuple<string, string>(ffmpegPath, ffprobePath);
         }
 
-        private void SetProbePathFromEncoderPath(string appPath)
+        private string GetProbePathFromEncoderPath(string appPath)
         {
-            FFProbePath = Directory.GetFiles(Path.GetDirectoryName(appPath))
+            return Directory.GetFiles(Path.GetDirectoryName(appPath))
                 .FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), "ffprobe", StringComparison.OrdinalIgnoreCase));
         }
 

+ 3 - 0
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -1136,6 +1136,9 @@
     <Compile Include="..\MediaBrowser.Model\Sync\SyncTarget.cs">
       <Link>Sync\SyncTarget.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\System\Architecture.cs">
+      <Link>System\Architecture.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\System\LogFile.cs">
       <Link>System\LogFile.cs</Link>
     </Compile>

+ 3 - 0
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -1101,6 +1101,9 @@
     <Compile Include="..\MediaBrowser.Model\Sync\SyncTarget.cs">
       <Link>Sync\SyncTarget.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\System\Architecture.cs">
+      <Link>System\Architecture.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\System\LogFile.cs">
       <Link>System\LogFile.cs</Link>
     </Compile>

+ 1 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -395,6 +395,7 @@
     <Compile Include="Sync\SyncProfileOption.cs" />
     <Compile Include="Sync\SyncQualityOption.cs" />
     <Compile Include="Sync\SyncTarget.cs" />
+    <Compile Include="System\Architecture.cs" />
     <Compile Include="System\LogFile.cs" />
     <Compile Include="System\PublicSystemInfo.cs" />
     <Compile Include="Updates\CheckForUpdateResult.cs" />

+ 9 - 0
MediaBrowser.Model/System/Architecture.cs

@@ -0,0 +1,9 @@
+namespace MediaBrowser.Model.System
+{
+    public enum Architecture
+    {
+        X86 = 0,
+        X64 = 1,
+        Arm = 2
+    }
+}

+ 2 - 0
MediaBrowser.Model/System/SystemInfo.cs

@@ -154,6 +154,8 @@ namespace MediaBrowser.Model.System
 
         public bool HasExternalEncoder { get; set; }
 
+        public Architecture SystemArchitecture { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="SystemInfo" /> class.
         /// </summary>

+ 19 - 5
MediaBrowser.Providers/Movies/MovieDbSearch.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies
 
         internal static string ApiKey = "f6bd687ffa63cd282b6ff2c6877f2669";
         internal static string AcceptHeader = "application/json,image/*";
-        
+
         private readonly ILogger _logger;
         private readonly IJsonSerializer _json;
         private readonly ILibraryManager _libraryManager;
@@ -54,6 +54,11 @@ namespace MediaBrowser.Providers.Movies
             var name = idInfo.Name;
             var year = idInfo.Year;
 
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                return new List<RemoteSearchResult>();
+            }
+
             var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
 
             var tmdbImageUrl = tmdbSettings.images.secure_base_url + "original";
@@ -73,7 +78,7 @@ namespace MediaBrowser.Providers.Movies
             //var searchType = item is BoxSet ? "collection" : "movie";
 
             var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
-            
+
             if (results.Count == 0)
             {
                 //try in english if wasn't before
@@ -123,19 +128,23 @@ namespace MediaBrowser.Providers.Movies
             });
         }
 
-        private async Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
+        private Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
         {
             switch (type)
             {
                 case "tv":
-                    return await GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken);
+                    return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken);
                 default:
-                    return await GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken);
+                    return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken);
             }
         }
 
         private async Task<List<RemoteSearchResult>> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentException("name");
+            }
 
             var url3 = string.Format(Search3, WebUtility.UrlEncode(name), ApiKey, language, type);
 
@@ -189,6 +198,11 @@ namespace MediaBrowser.Providers.Movies
 
         private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
         {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentException("name");
+            }
+
             var url3 = string.Format(Search3, WebUtility.UrlEncode(name), ApiKey, language, "tv");
 
             using (var json = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions

+ 9 - 1
MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs

@@ -339,7 +339,14 @@ namespace MediaBrowser.Providers.Music
             {
                 var urls = await RefreshMzbUrls().ConfigureAwait(false);
 
-                _chosenUrl = urls[new Random().Next(0, urls.Count - 1)];
+                if (urls.Count > 1)
+                {
+                    _chosenUrl = urls[new Random().Next(0, urls.Count)];
+                }
+                else
+                {
+                    _chosenUrl = urls[0];
+                }
             }
 
             return _chosenUrl;
@@ -361,6 +368,7 @@ namespace MediaBrowser.Providers.Music
                 {
                     list = _json.DeserializeFromStream<List<MbzUrl>>(stream);
                 }
+                _lastMbzUrlQueryTicks = DateTime.UtcNow.Ticks;
             }
             catch (Exception ex)
             {

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

@@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.Music
                         {
                             if (string.Equals(child.Name, "name", StringComparison.OrdinalIgnoreCase))
                             {
-                                name = node.InnerText;
+                                name = child.InnerText;
                                 break;
                             }
                         }

+ 200 - 0
MediaBrowser.Providers/Omdb/OmdbProvider.cs

@@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Serialization;
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -114,6 +115,106 @@ namespace MediaBrowser.Providers.Omdb
                 ParseAdditionalMetadata(item, result);
         }
 
+        public async Task<bool> FetchEpisodeData(BaseItem item, int episodeNumber, int seasonNumber, string imdbId, string language, string country, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrWhiteSpace(imdbId))
+            {
+                throw new ArgumentNullException("imdbId");
+            }
+
+            var seasonResult = await GetSeasonRootObject(imdbId, seasonNumber, cancellationToken);
+
+            RootObject result = null;
+
+            foreach (var episode in seasonResult.Episodes)
+            {
+                if (episode.Episode == episodeNumber)
+                {
+                    result = episode;
+                    break;
+                }
+            }
+
+            if (result == null)
+            {
+                return false;
+            }
+
+
+            // Only take the name and rating if the user's language is set to english, since Omdb has no localization
+            if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+            {
+                item.Name = result.Title;
+
+                if (string.Equals(country, "us", StringComparison.OrdinalIgnoreCase))
+                {
+                    item.OfficialRating = result.Rated;
+                }
+            }
+
+            int year;
+
+            if (!string.IsNullOrEmpty(result.Year)
+                && int.TryParse(result.Year, NumberStyles.Number, _usCulture, out year)
+                && year >= 0)
+            {
+                item.ProductionYear = year;
+            }
+
+            var hasCriticRating = item as IHasCriticRating;
+            if (hasCriticRating != null)
+            {
+                // Seeing some bogus RT data on omdb for series, so filter it out here
+                // RT doesn't even have tv series
+                int tomatoMeter;
+
+                if (!string.IsNullOrEmpty(result.tomatoMeter)
+                    && int.TryParse(result.tomatoMeter, NumberStyles.Integer, _usCulture, out tomatoMeter)
+                    && tomatoMeter >= 0)
+                {
+                    hasCriticRating.CriticRating = tomatoMeter;
+                }
+
+                if (!string.IsNullOrEmpty(result.tomatoConsensus)
+                    && !string.Equals(result.tomatoConsensus, "No consensus yet.", StringComparison.OrdinalIgnoreCase))
+                {
+                    hasCriticRating.CriticRatingSummary = WebUtility.HtmlDecode(result.tomatoConsensus);
+                }
+            }
+
+            int voteCount;
+
+            if (!string.IsNullOrEmpty(result.imdbVotes)
+                && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out voteCount)
+                && voteCount >= 0)
+            {
+                item.VoteCount = voteCount;
+            }
+
+            float imdbRating;
+
+            if (!string.IsNullOrEmpty(result.imdbRating)
+                && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out imdbRating)
+                && imdbRating >= 0)
+            {
+                item.CommunityRating = imdbRating;
+            }
+
+            if (!string.IsNullOrEmpty(result.Website))
+            {
+                item.HomePageUrl = result.Website;
+            }
+
+            if (!string.IsNullOrWhiteSpace(result.imdbID))
+            {
+                item.SetProviderId(MetadataProviders.Imdb, result.imdbID);
+            }
+
+            ParseAdditionalMetadata(item, result);
+
+            return true;
+        }
+
         internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
         {
             var path = await EnsureItemInfo(imdbId, cancellationToken);
@@ -133,6 +234,40 @@ namespace MediaBrowser.Providers.Omdb
             return result;
         }
 
+        internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
+        {
+            var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken);
+
+            string resultString;
+
+            using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 131072))
+            {
+                using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
+                {
+                    resultString = reader.ReadToEnd();
+                    resultString = resultString.Replace("\"N/A\"", "\"\"");
+                }
+            }
+
+            var result = _jsonSerializer.DeserializeFromString<SeasonRootObject>(resultString);
+            return result;
+        }
+
+        internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
+        {
+            string id;
+            if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out id) && !string.IsNullOrEmpty(id))
+            {
+                // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
+                if (!string.IsNullOrWhiteSpace(id))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
         {
             if (string.IsNullOrWhiteSpace(imdbId))
@@ -173,6 +308,46 @@ namespace MediaBrowser.Providers.Omdb
             return path;
         }
 
+        private async Task<string> EnsureSeasonInfo(string seriesImdbId, int seasonId, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrWhiteSpace(seriesImdbId))
+            {
+                throw new ArgumentNullException("imdbId");
+            }
+
+            var imdbParam = seriesImdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? seriesImdbId : "tt" + seriesImdbId;
+
+            var path = GetSeasonFilePath(imdbParam, seasonId);
+
+            var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+            if (fileInfo.Exists)
+            {
+                // If it's recent or automatic updates are enabled, don't re-download
+                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 3)
+                {
+                    return path;
+                }
+            }
+
+            var url = string.Format("https://www.omdbapi.com/?i={0}&season={1}&detail=full", imdbParam, seasonId);
+
+            using (var stream = await _httpClient.Get(new HttpRequestOptions
+            {
+                Url = url,
+                ResourcePool = ResourcePool,
+                CancellationToken = cancellationToken
+
+            }).ConfigureAwait(false))
+            {
+                var rootObject = _jsonSerializer.DeserializeFromStream<SeasonRootObject>(stream);
+                _fileSystem.CreateDirectory(Path.GetDirectoryName(path));
+                _jsonSerializer.SerializeToFile(rootObject, path);
+            }
+
+            return path;
+        }
+
         internal string GetDataFilePath(string imdbId)
         {
             if (string.IsNullOrEmpty(imdbId))
@@ -187,6 +362,20 @@ namespace MediaBrowser.Providers.Omdb
             return Path.Combine(dataPath, filename);
         }
 
+        internal string GetSeasonFilePath(string imdbId, int seasonId)
+        {
+            if (string.IsNullOrEmpty(imdbId))
+            {
+                throw new ArgumentNullException("imdbId");
+            }
+
+            var dataPath = Path.Combine(_configurationManager.ApplicationPaths.CachePath, "omdb");
+
+            var filename = string.Format("{0}_season_{1}.json", imdbId, seasonId);
+
+            return Path.Combine(dataPath, filename);
+        }
+
         private void ParseAdditionalMetadata(BaseItem item, RootObject result)
         {
             // Grab series genres because imdb data is better than tvdb. Leave movies alone
@@ -238,12 +427,23 @@ namespace MediaBrowser.Providers.Omdb
             return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
         }
 
+        internal class SeasonRootObject
+        {
+            public string Title { get; set; }
+            public string seriesID { get; set; }
+            public int Season { get; set; }
+            public int? totalSeasons { get; set; }
+            public RootObject[] Episodes { get; set; }
+            public string Response { get; set; }
+        }
+
         internal class RootObject
         {
             public string Title { get; set; }
             public string Year { get; set; }
             public string Rated { get; set; }
             public string Released { get; set; }
+            public int Episode { get; set; }
             public string Runtime { get; set; }
             public string Genre { get; set; }
             public string Director { get; set; }

+ 4 - 18
MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs

@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.TV
 
         public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
         {
-            var result = new MetadataResult<Episode>
+            var result = new MetadataResult<Episode>()
             {
                 Item = new Episode()
             };
@@ -53,30 +53,16 @@ namespace MediaBrowser.Providers.TV
                 return result;
             }
 
-            var imdbId = info.GetProviderId(MetadataProviders.Imdb);
-            if (string.IsNullOrWhiteSpace(imdbId))
+            if (OmdbProvider.IsValidSeries(info.SeriesProviderIds) && info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
             {
-                imdbId = await GetEpisodeImdbId(info, cancellationToken).ConfigureAwait(false);
-            }
-
-            if (!string.IsNullOrEmpty(imdbId))
-            {
-                result.Item.SetProviderId(MetadataProviders.Imdb, imdbId);
-                result.HasMetadata = true;
+                var seriesImdbId = info.SeriesProviderIds[MetadataProviders.Imdb.ToString()];
 
-                await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+                result.HasMetadata = await new OmdbProvider(_jsonSerializer, _httpClient, _fileSystem, _configurationManager).FetchEpisodeData(result.Item, info.IndexNumber.Value, info.ParentIndexNumber.Value, seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
             }
 
             return result;
         }
 
-        private async Task<string> GetEpisodeImdbId(EpisodeInfo info, CancellationToken cancellationToken)
-        {
-            var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
-            var first = results.FirstOrDefault();
-            return first == null ? null : first.GetProviderId(MetadataProviders.Imdb);
-        }
-
         public int Order
         {
             get

+ 4 - 0
MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs

@@ -104,6 +104,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
                     {
                         info.DeviceId = tokenInfo.DeviceId;
                     }
+                    if (string.IsNullOrWhiteSpace(info.Version))
+                    {
+                        info.Version = tokenInfo.AppVersion;
+                    }
                 }
                 else
                 {

+ 3 - 3
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -461,7 +461,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             return CreateTimer(info, cancellationToken);
         }
 
-        public  Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+        public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
         {
             return CreateSeriesTimer(info, cancellationToken);
         }
@@ -1011,7 +1011,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
                     Action onStarted = () =>
                     {
                         timer.Status = RecordingStatus.InProgress;
-                        _timerProvider.AddOrUpdate(timer);
+                        _timerProvider.AddOrUpdate(timer, false);
 
                         result.Item3.Release();
                         isResourceOpen = false;
@@ -1060,7 +1060,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             if (recordingStatus == RecordingStatus.Completed)
             {
                 timer.Status = RecordingStatus.Completed;
-                _timerProvider.AddOrUpdate(timer);
+                _timerProvider.AddOrUpdate(timer, false);
 
                 OnSuccessfulRecording(info.IsSeries, recordPath);
                 _timerProvider.Delete(timer);

+ 20 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -72,6 +72,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
+        public void AddOrUpdate(TimerInfo item, bool resetTimer)
+        {
+            if (resetTimer)
+            {
+                AddOrUpdate(item);
+                return;
+            }
+
+            var list = GetAll().ToList();
+
+            if (!list.Any(i => EqualityComparer(i, item)))
+            {
+                base.Add(item);
+            }
+            else
+            {
+                base.Update(item);
+            }
+        }
+
         public override void Add(TimerInfo item)
         {
             if (string.IsNullOrWhiteSpace(item.Id))

+ 2 - 1
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -127,7 +127,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
 
             connection.RunQueries(new[]
             {
-                "pragma temp_store = memory"
+                "pragma temp_store = memory",
+                "PRAGMA main.locking_mode=EXCLUSIVE"
 
             }, Logger);
 

+ 5 - 4
MediaBrowser.Server.Mono/Native/BaseMonoApp.cs

@@ -9,6 +9,7 @@ using System.Collections.Generic;
 using System.Reflection;
 using System.Text.RegularExpressions;
 using MediaBrowser.Controller.Power;
+using MediaBrowser.Model.System;
 using MediaBrowser.Server.Implementations.Persistence;
 using MediaBrowser.Server.Startup.Common.FFMpeg;
 using OperatingSystem = MediaBrowser.Server.Startup.Common.OperatingSystem;
@@ -176,7 +177,7 @@ namespace MediaBrowser.Server.Mono.Native
             }
             else if (string.Equals(uname.machine, "x86_64", StringComparison.OrdinalIgnoreCase))
             {
-                info.SystemArchitecture = Architecture.X86_X64;
+                info.SystemArchitecture = Architecture.X64;
             }
             else if (uname.machine.StartsWith("arm", StringComparison.OrdinalIgnoreCase))
             {
@@ -260,7 +261,7 @@ namespace MediaBrowser.Server.Mono.Native
 
                     switch (environment.SystemArchitecture)
                     {
-                        case Architecture.X86_X64:
+                        case Architecture.X64:
                             info.Version = "20160124";
                             break;
                         case Architecture.X86:
@@ -283,7 +284,7 @@ namespace MediaBrowser.Server.Mono.Native
 
                     switch (environment.SystemArchitecture)
                     {
-                        case Architecture.X86_X64:
+                        case Architecture.X64:
                             return new[]
                             {
                                 "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/osx/ffmpeg-x64-2.8.5.7z"
@@ -300,7 +301,7 @@ namespace MediaBrowser.Server.Mono.Native
 
                     switch (environment.SystemArchitecture)
                     {
-                        case Architecture.X86_X64:
+                        case Architecture.X64:
                             return new[]
                             {
                                 "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-64bit-static.7z"

+ 10 - 4
MediaBrowser.Server.Startup.Common/ApplicationHost.cs

@@ -647,15 +647,20 @@ namespace MediaBrowser.Server.Startup.Common
         /// <returns>Task.</returns>
         private async Task RegisterMediaEncoder(IProgress<double> progress)
         {
+            string encoderPath = null;
+            string probePath = null;
+
             var info = await new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, NativeApp.Environment, NativeApp.GetFfmpegInstallInfo())
                 .GetFFMpegInfo(NativeApp.Environment, _startupOptions, progress).ConfigureAwait(false);
 
-            _hasExternalEncoder = string.Equals(info.Version, "custom", StringComparison.OrdinalIgnoreCase);
+            encoderPath = info.EncoderPath;
+            probePath = info.ProbePath;
+            _hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase);
 
             var mediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"),
                 JsonSerializer,
-                info.EncoderPath,
-                info.ProbePath,
+                encoderPath,
+                probePath,
                 _hasExternalEncoder,
                 ServerConfigurationManager,
                 FileSystemManager,
@@ -1145,7 +1150,8 @@ namespace MediaBrowser.Server.Startup.Common
                 ServerName = FriendlyName,
                 LocalAddress = localAddress,
                 SupportsLibraryMonitor = SupportsLibraryMonitor,
-                HasExternalEncoder = _hasExternalEncoder
+                HasExternalEncoder = _hasExternalEncoder,
+                SystemArchitecture = NativeApp.Environment.SystemArchitecture
             };
         }
 

+ 5 - 13
MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs

@@ -53,13 +53,17 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg
                 {
                     ProbePath = customffProbePath,
                     EncoderPath = customffMpegPath,
-                    Version = "custom"
+                    Version = "external"
                 };
             }
 
             var downloadInfo = _ffmpegInstallInfo;
 
             var version = downloadInfo.Version;
+            if (string.Equals(version, "0", StringComparison.OrdinalIgnoreCase))
+            {
+                return new FFMpegInfo();
+            }
 
             if (string.Equals(version, "path", StringComparison.OrdinalIgnoreCase))
             {
@@ -175,18 +179,6 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg
             return null;
         }
 
-        private async void DownloadFFMpegInBackground(FFMpegInstallInfo downloadinfo, string directory)
-        {
-            try
-            {
-                await DownloadFFMpeg(downloadinfo, directory, new Progress<double>()).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                _logger.ErrorException("Error downloading ffmpeg", ex);
-            }
-        }
-
         private async Task DownloadFFMpeg(FFMpegInstallInfo downloadinfo, string directory, IProgress<double> progress)
         {
             foreach (var url in downloadinfo.DownloadUrls)

+ 2 - 8
MediaBrowser.Server.Startup.Common/NativeEnvironment.cs

@@ -1,4 +1,5 @@
-
+using MediaBrowser.Model.System;
+
 namespace MediaBrowser.Server.Startup.Common
 {
     public class NativeEnvironment
@@ -15,11 +16,4 @@ namespace MediaBrowser.Server.Startup.Common
         Bsd = 2,
         Linux = 3
     }
-
-    public enum Architecture
-    {
-        X86 = 0,
-        X86_X64 = 1,
-        Arm = 2
-    }
 }

+ 3 - 24
MediaBrowser.ServerApplication/Native/WindowsApp.cs

@@ -10,6 +10,7 @@ using System.Reflection;
 using System.Windows.Forms;
 using CommonIO;
 using MediaBrowser.Controller.Power;
+using MediaBrowser.Model.System;
 using MediaBrowser.Server.Implementations.Persistence;
 using MediaBrowser.Server.Startup.Common.FFMpeg;
 using OperatingSystem = MediaBrowser.Server.Startup.Common.OperatingSystem;
@@ -53,7 +54,7 @@ namespace MediaBrowser.ServerApplication.Native
                 return new NativeEnvironment
                 {
                     OperatingSystem = OperatingSystem.Windows,
-                    SystemArchitecture = System.Environment.Is64BitOperatingSystem ? Architecture.X86_X64 : Architecture.X86,
+                    SystemArchitecture = System.Environment.Is64BitOperatingSystem ? Architecture.X64 : Architecture.X86,
                     OperatingSystemVersionString = System.Environment.OSVersion.VersionString
                 };
             }
@@ -158,9 +159,7 @@ namespace MediaBrowser.ServerApplication.Native
 
             info.FFMpegFilename = "ffmpeg.exe";
             info.FFProbeFilename = "ffprobe.exe";
-            info.Version = "20160410";
-            info.ArchiveType = "7z";
-            info.DownloadUrls = GetDownloadUrls();
+            info.Version = "0";
 
             return info;
         }
@@ -205,25 +204,5 @@ namespace MediaBrowser.ServerApplication.Native
         {
             ((Process)sender).Dispose();
         }
-
-        private string[] GetDownloadUrls()
-        {
-            switch (Environment.SystemArchitecture)
-            {
-                case Architecture.X86_X64:
-                    return new[]
-                    {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win64.7z",
-                                "https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20160409-git-0c90b2e-win64-static.7z"
-                            };
-                case Architecture.X86:
-                    return new[]
-                    {
-                                "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win32.7z",
-                                "https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20160409-git-0c90b2e-win32-static.7z"
-                            };
-            }
-            return new string[] { };
-        }
     }
 }

+ 9 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -374,6 +374,12 @@
     <Content Include="dashboard-ui\scripts\tvlatest.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\wizardcomponents.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="dashboard-ui\scripts\wizardcontroller.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\wizardlivetvguide.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -1094,6 +1100,9 @@
     <Content Include="dashboard-ui\wizardagreement.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\wizardcomponents.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\wizardlivetvguide.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>