Ver Fonte

beginning remote subtitle downloading

Luke Pulverenti há 11 anos atrás
pai
commit
0d025f7fb6
49 ficheiros alterados com 1030 adições e 296 exclusões
  1. 1 1
      MediaBrowser.Api/ChannelService.cs
  2. 87 3
      MediaBrowser.Api/Images/ImageByNameService.cs
  3. 2 2
      MediaBrowser.Api/Images/ImageService.cs
  4. 23 4
      MediaBrowser.Api/ItemLookupService.cs
  5. 1 1
      MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs
  6. 3 0
      MediaBrowser.Api/UserLibrary/ItemsService.cs
  7. 9 4
      MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs
  8. 4 2
      MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
  9. 6 10
      MediaBrowser.Common/Net/HttpRequestOptions.cs
  10. 2 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  11. 50 0
      MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
  12. 15 6
      MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs
  13. 4 0
      MediaBrowser.Dlna/PlayTo/Device.cs
  14. 31 0
      MediaBrowser.Dlna/PlayTo/DlnaController.cs
  15. 1 1
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  16. 1 1
      MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
  17. 2 2
      MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
  18. 2 2
      MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
  19. 2 2
      MediaBrowser.MediaEncoding/Subtitles/SubtitleTrackInfo.cs
  20. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  21. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  22. 1 1
      MediaBrowser.Model/ApiClient/IApiClient.cs
  23. 17 0
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  24. 1 1
      MediaBrowser.Model/Dto/StreamOptions.cs
  25. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  26. 19 0
      MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs
  27. 4 0
      MediaBrowser.Model/Querying/ItemFilter.cs
  28. 2 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  29. 10 4
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  30. 47 10
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  31. 140 0
      MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
  32. 81 57
      MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs
  33. 141 0
      MediaBrowser.Providers/Subtitles/SubtitleManager.cs
  34. 1 1
      MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
  35. 7 1
      MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
  36. 4 2
      MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs
  37. 0 44
      MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs
  38. 14 33
      MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs
  39. 80 1
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  40. 0 0
      MediaBrowser.Server.Implementations/Localization/countries.json
  41. 0 0
      MediaBrowser.Server.Implementations/Localization/cultures.json
  42. 2 1
      MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
  43. 9 2
      MediaBrowser.ServerApplication/ApplicationHost.cs
  44. 48 33
      MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs
  45. 77 58
      MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs
  46. 1 1
      MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs
  47. 3 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  48. 52 0
      OpenSubtitlesHandler/OpenSubtitles.cs
  49. 16 4
      OpenSubtitlesHandler/Utilities.cs

+ 1 - 1
MediaBrowser.Api/ChannelService.cs

@@ -67,7 +67,7 @@ namespace MediaBrowser.Api
         [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         [ApiMember(Name = "SortOrder", Description = "Sort Order - Ascending,Descending", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public SortOrder? SortOrder { get; set; }
         public SortOrder? SortOrder { get; set; }
 
 
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsRecentlyAdded, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string Filters { get; set; }
         public string Filters { get; set; }
 
 
         [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         [ApiMember(Name = "SortBy", Description = "Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]

+ 87 - 3
MediaBrowser.Api/Images/ImageByNameService.cs

@@ -1,8 +1,10 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using ServiceStack;
 using ServiceStack;
 using System;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 
 
@@ -70,6 +72,32 @@ namespace MediaBrowser.Api.Images
         public string Theme { get; set; }
         public string Theme { get; set; }
     }
     }
 
 
+    [Route("/Images/MediaInfo", "GET")]
+    [Api(Description = "Gets all media info image by name")]
+    public class GetMediaInfoImages : IReturn<List<ImageByNameInfo>>
+    {
+    }
+
+    [Route("/Images/Ratings", "GET")]
+    [Api(Description = "Gets all rating images by name")]
+    public class GetRatingImages : IReturn<List<ImageByNameInfo>>
+    {
+    }
+
+    [Route("/Images/General", "GET")]
+    [Api(Description = "Gets all general images by name")]
+    public class GetGeneralImages : IReturn<List<ImageByNameInfo>>
+    {
+    }
+
+    public class ImageByNameInfo
+    {
+        public string Name { get; set; }
+        public string Theme { get; set; }
+        public long FileLength { get; set; }
+        public string Format { get; set; }
+    }
+
     /// <summary>
     /// <summary>
     /// Class ImageByNameService
     /// Class ImageByNameService
     /// </summary>
     /// </summary>
@@ -89,6 +117,60 @@ namespace MediaBrowser.Api.Images
             _appPaths = appPaths;
             _appPaths = appPaths;
         }
         }
 
 
+        public object Get(GetMediaInfoImages request)
+        {
+            return ToOptimizedResult(GetImageList(_appPaths.MediaInfoImagesPath));
+        }
+
+        public object Get(GetRatingImages request)
+        {
+            return ToOptimizedResult(GetImageList(_appPaths.RatingsPath));
+        }
+
+        public object Get(GetGeneralImages request)
+        {
+            return ToOptimizedResult(GetImageList(_appPaths.GeneralPath));
+        }
+
+        private List<ImageByNameInfo> GetImageList(string path)
+        {
+            try
+            {
+                return new DirectoryInfo(path)
+                    .GetFiles("*", SearchOption.AllDirectories)
+                    .Where(i => BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparer.Ordinal))
+                    .Select(i => new ImageByNameInfo
+                    {
+                        Name = Path.GetFileNameWithoutExtension(i.FullName),
+                        FileLength = i.Length,
+                        Theme = GetThemeName(i.FullName, path),
+                        Format = i.Extension.ToLower().TrimStart('.')
+                    })
+                    .OrderBy(i => i.Name)
+                    .ToList();
+            }
+            catch (DirectoryNotFoundException)
+            {
+                return new List<ImageByNameInfo>();
+            }
+        }
+
+        private string GetThemeName(string path, string rootImagePath)
+        {
+            var parentName = Path.GetDirectoryName(path);
+
+            if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            parentName = Path.GetFileName(parentName);
+
+            return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ?
+                null :
+                parentName;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the specified request.
         /// Gets the specified request.
         /// </summary>
         /// </summary>
@@ -118,7 +200,8 @@ namespace MediaBrowser.Api.Images
 
 
             if (Directory.Exists(themeFolder))
             if (Directory.Exists(themeFolder))
             {
             {
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, request.Name + i))
+                var path = BaseItem.SupportedImageExtensions
+                    .Select(i => Path.Combine(themeFolder, request.Name + i))
                     .FirstOrDefault(File.Exists);
                     .FirstOrDefault(File.Exists);
 
 
                 if (!string.IsNullOrEmpty(path))
                 if (!string.IsNullOrEmpty(path))
@@ -134,7 +217,8 @@ namespace MediaBrowser.Api.Images
                 // Avoid implicitly captured closure
                 // Avoid implicitly captured closure
                 var currentRequest = request;
                 var currentRequest = request;
 
 
-                var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
+                var path = BaseItem.SupportedImageExtensions
+                    .Select(i => Path.Combine(allFolder, currentRequest.Name + i))
                     .FirstOrDefault(File.Exists);
                     .FirstOrDefault(File.Exists);
 
 
                 if (!string.IsNullOrEmpty(path))
                 if (!string.IsNullOrEmpty(path))
@@ -175,7 +259,7 @@ namespace MediaBrowser.Api.Images
 
 
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
                     .FirstOrDefault(File.Exists);
                     .FirstOrDefault(File.Exists);
-                
+
                 if (!string.IsNullOrEmpty(path))
                 if (!string.IsNullOrEmpty(path))
                 {
                 {
                     return ToStaticFileResult(path);
                     return ToStaticFileResult(path);

+ 2 - 2
MediaBrowser.Api/Images/ImageService.cs

@@ -1,5 +1,4 @@
-using System.Globalization;
-using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -14,6 +13,7 @@ using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using ServiceStack.Web;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;

+ 23 - 4
MediaBrowser.Api/ItemLookupService.cs

@@ -1,13 +1,13 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using MediaBrowser.Model.Providers;
 using ServiceStack;
 using ServiceStack;
@@ -32,6 +32,16 @@ namespace MediaBrowser.Api
         public string Id { get; set; }
         public string Id { get; set; }
     }
     }
 
 
+    [Route("/Items/{Id}/RemoteSearch/Subtitles/{Language}", "GET")]
+    public class SearchRemoteSubtitles : IReturn<List<RemoteSubtitleInfo>>
+    {
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Language", Description = "Language", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Language { get; set; }
+    }
+
     [Route("/Items/RemoteSearch/Movie", "POST")]
     [Route("/Items/RemoteSearch/Movie", "POST")]
     [Api(Description = "Gets external id infos for an item")]
     [Api(Description = "Gets external id infos for an item")]
     public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
     public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
@@ -107,19 +117,28 @@ namespace MediaBrowser.Api
 
 
     public class ItemLookupService : BaseApiService
     public class ItemLookupService : BaseApiService
     {
     {
-        private readonly IDtoService _dtoService;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly ISubtitleManager _subtitleManager;
 
 
-        public ItemLookupService(IDtoService dtoService, IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager)
+        public ItemLookupService(IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager, ISubtitleManager subtitleManager)
         {
         {
-            _dtoService = dtoService;
             _providerManager = providerManager;
             _providerManager = providerManager;
             _appPaths = appPaths;
             _appPaths = appPaths;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
+            _subtitleManager = subtitleManager;
+        }
+
+        public object Get(SearchRemoteSubtitles request)
+        {
+            var video = (Video)_libraryManager.GetItemById(request.Id);
+
+            var response = _subtitleManager.SearchSubtitles(video, request.Language, CancellationToken.None).Result;
+
+            return ToOptimizedResult(response);
         }
         }
 
 
         public object Get(GetExternalIdInfos request)
         public object Get(GetExternalIdInfos request)

+ 1 - 1
MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs

@@ -69,7 +69,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// Filters to apply to the results
         /// Filters to apply to the results
         /// </summary>
         /// </summary>
         /// <value>The filters.</value>
         /// <value>The filters.</value>
-        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsRecentlyAdded, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
+        [ApiMember(Name = "Filters", Description = "Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
         public string Filters { get; set; }
         public string Filters { get; set; }
 
 
         /// <summary>
         /// <summary>

+ 3 - 0
MediaBrowser.Api/UserLibrary/ItemsService.cs

@@ -521,6 +521,9 @@ namespace MediaBrowser.Api.UserLibrary
 
 
                 case ItemFilter.IsNotFolder:
                 case ItemFilter.IsNotFolder:
                     return items.Where(item => !item.IsFolder);
                     return items.Where(item => !item.IsFolder);
+
+                case ItemFilter.IsRecentlyAdded:
+                    return items.Where(item => (DateTime.UtcNow - item.DateCreated).TotalDays <= 10);
             }
             }
 
 
             return items;
             return items;

+ 9 - 4
MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs

@@ -114,9 +114,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
 
 
             request.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
             request.AutomaticDecompression = enableHttpCompression ? DecompressionMethods.Deflate : DecompressionMethods.None;
 
 
-            request.CachePolicy = options.CachePolicy == Net.HttpRequestCachePolicy.None ?
-                new RequestCachePolicy(RequestCacheLevel.BypassCache) :
-                new RequestCachePolicy(RequestCacheLevel.Revalidate);
+            request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
 
 
             request.ConnectionGroupName = GetHostFromUrl(options.Url);
             request.ConnectionGroupName = GetHostFromUrl(options.Url);
             request.KeepAlive = true;
             request.KeepAlive = true;
@@ -124,6 +122,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             request.Pipelined = true;
             request.Pipelined = true;
             request.Timeout = 20000;
             request.Timeout = 20000;
 
 
+            if (!string.IsNullOrEmpty(options.Host))
+            {
+                request.Host = options.Host;
+            }
+
 #if !__MonoCS__
 #if !__MonoCS__
             // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
             // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
             // May need to remove this for mono
             // May need to remove this for mono
@@ -234,9 +237,11 @@ namespace MediaBrowser.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);
+                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";
+                
                 httpWebRequest.ContentLength = bytes.Length;
                 httpWebRequest.ContentLength = bytes.Length;
                 httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length);
                 httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length);
             }
             }

+ 4 - 2
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -25,7 +25,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         private readonly ILogger _logger;
         private readonly ILogger _logger;
 
 
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
-        
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
         /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
         /// </summary>
         /// </summary>
@@ -74,9 +74,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
 
 
             progress.Report(90);
             progress.Report(90);
 
 
+            minDateModified = DateTime.UtcNow.AddDays(-3);
+
             try
             try
             {
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, DateTime.MaxValue, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
             }
             }
             catch (DirectoryNotFoundException)
             catch (DirectoryNotFoundException)
             {
             {

+ 6 - 10
MediaBrowser.Common/Net/HttpRequestOptions.cs

@@ -52,6 +52,12 @@ namespace MediaBrowser.Common.Net
             }
             }
         }
         }
 
 
+        /// <summary>
+        /// Gets or sets the host.
+        /// </summary>
+        /// <value>The host.</value>
+        public string Host { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the progress.
         /// Gets or sets the progress.
         /// </summary>
         /// </summary>
@@ -76,8 +82,6 @@ namespace MediaBrowser.Common.Net
         public bool LogRequest { get; set; }
         public bool LogRequest { get; set; }
 
 
         public bool LogErrorResponseBody { get; set; }
         public bool LogErrorResponseBody { get; set; }
-        
-        public HttpRequestCachePolicy CachePolicy { get; set; }
 
 
         private string GetHeaderValue(string name)
         private string GetHeaderValue(string name)
         {
         {
@@ -96,17 +100,9 @@ namespace MediaBrowser.Common.Net
             EnableHttpCompression = true;
             EnableHttpCompression = true;
             BufferContent = true;
             BufferContent = true;
 
 
-            CachePolicy = HttpRequestCachePolicy.None;
-
             RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
 
             LogRequest = true;
             LogRequest = true;
         }
         }
     }
     }
-
-    public enum HttpRequestCachePolicy
-    {
-        None = 1,
-        Validate = 2
-    }
 }
 }

+ 2 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -191,7 +191,8 @@
     <Compile Include="Providers\IMetadataProvider.cs" />
     <Compile Include="Providers\IMetadataProvider.cs" />
     <Compile Include="Providers\IMetadataService.cs" />
     <Compile Include="Providers\IMetadataService.cs" />
     <Compile Include="Providers\IRemoteMetadataProvider.cs" />
     <Compile Include="Providers\IRemoteMetadataProvider.cs" />
-    <Compile Include="Providers\ISubtitleProvider.cs" />
+    <Compile Include="Subtitles\ISubtitleManager.cs" />
+    <Compile Include="Subtitles\ISubtitleProvider.cs" />
     <Compile Include="Providers\ItemLookupInfo.cs" />
     <Compile Include="Providers\ItemLookupInfo.cs" />
     <Compile Include="Providers\MetadataRefreshOptions.cs" />
     <Compile Include="Providers\MetadataRefreshOptions.cs" />
     <Compile Include="Providers\NameParser.cs" />
     <Compile Include="Providers\NameParser.cs" />

+ 50 - 0
MediaBrowser.Controller/Subtitles/ISubtitleManager.cs

@@ -0,0 +1,50 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Providers;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Subtitles
+{
+    public interface ISubtitleManager
+    {
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="subtitleProviders">The subtitle providers.</param>
+        void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders);
+
+        /// <summary>
+        /// Searches the subtitles.
+        /// </summary>
+        /// <param name="video">The video.</param>
+        /// <param name="language">The language.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+        Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(Video video,
+            string language,
+            CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Searches the subtitles.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+        Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, 
+            CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Downloads the subtitles.
+        /// </summary>
+        /// <param name="video">The video.</param>
+        /// <param name="subtitleId">The subtitle identifier.</param>
+        /// <param name="providerName">Name of the provider.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task DownloadSubtitles(Video video, 
+            string subtitleId, 
+            string providerName, 
+            CancellationToken cancellationToken);
+    }
+}

+ 15 - 6
MediaBrowser.Controller/Providers/ISubtitleProvider.cs → MediaBrowser.Controller/Subtitles/ISubtitleProvider.cs

@@ -1,11 +1,12 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 
 
-namespace MediaBrowser.Controller.Providers
+namespace MediaBrowser.Controller.Subtitles
 {
 {
     public interface ISubtitleProvider
     public interface ISubtitleProvider
     {
     {
@@ -22,12 +23,20 @@ namespace MediaBrowser.Controller.Providers
         IEnumerable<SubtitleMediaType> SupportedMediaTypes { get; }
         IEnumerable<SubtitleMediaType> SupportedMediaTypes { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the subtitles.
+        /// Searches the subtitles.
         /// </summary>
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
+        Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the subtitles.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{SubtitleResponse}.</returns>
         /// <returns>Task{SubtitleResponse}.</returns>
-        Task<SubtitleResponse> GetSubtitles(SubtitleRequest request, CancellationToken cancellationToken);
+        Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken);
     }
     }
 
 
     public enum SubtitleMediaType
     public enum SubtitleMediaType
@@ -38,12 +47,12 @@ namespace MediaBrowser.Controller.Providers
 
 
     public class SubtitleResponse
     public class SubtitleResponse
     {
     {
+        public string Language { get; set; }
         public string Format { get; set; }
         public string Format { get; set; }
-        public bool HasContent { get; set; }
         public Stream Stream { get; set; }
         public Stream Stream { get; set; }
     }
     }
 
 
-    public class SubtitleRequest : IHasProviderIds
+    public class SubtitleSearchRequest : IHasProviderIds
     {
     {
         public string Language { get; set; }
         public string Language { get; set; }
 
 
@@ -58,7 +67,7 @@ namespace MediaBrowser.Controller.Providers
         public int? ProductionYear { get; set; }
         public int? ProductionYear { get; set; }
         public Dictionary<string, string> ProviderIds { get; set; }
         public Dictionary<string, string> ProviderIds { get; set; }
 
 
-        public SubtitleRequest()
+        public SubtitleSearchRequest()
         {
         {
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
         }

+ 4 - 0
MediaBrowser.Dlna/PlayTo/Device.cs

@@ -77,6 +77,8 @@ namespace MediaBrowser.Dlna.PlayTo
         private readonly ILogger _logger;
         private readonly ILogger _logger;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
 
 
+        public DateTime DateLastActivity { get; private set; }
+
         public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
         public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
         {
         {
             Properties = deviceProperties;
             Properties = deviceProperties;
@@ -386,6 +388,8 @@ namespace MediaBrowser.Dlna.PlayTo
             {
             {
                 var transportState = await GetTransportInfo().ConfigureAwait(false);
                 var transportState = await GetTransportInfo().ConfigureAwait(false);
 
 
+                DateLastActivity = DateTime.UtcNow;
+
                 if (transportState.HasValue)
                 if (transportState.HasValue)
                 {
                 {
                     // If we're not playing anything no need to get additional data
                     // If we're not playing anything no need to get additional data

+ 31 - 0
MediaBrowser.Dlna/PlayTo/DlnaController.cs

@@ -51,6 +51,8 @@ namespace MediaBrowser.Dlna.PlayTo
             }
             }
         }
         }
 
 
+        private Timer _updateTimer;
+
         public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IDtoService dtoService, IImageProcessor imageProcessor, SsdpHandler ssdpHandler, string serverAddress)
         public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IDtoService dtoService, IImageProcessor imageProcessor, SsdpHandler ssdpHandler, string serverAddress)
         {
         {
             _session = session;
             _session = session;
@@ -75,6 +77,24 @@ namespace MediaBrowser.Dlna.PlayTo
             _device.Start();
             _device.Start();
 
 
             _ssdpHandler.MessageReceived += _SsdpHandler_MessageReceived;
             _ssdpHandler.MessageReceived += _SsdpHandler_MessageReceived;
+
+            _updateTimer = new Timer(updateTimer_Elapsed, null, 60000, 60000);
+        }
+
+        private async void updateTimer_Elapsed(object state)
+        {
+            if (DateTime.UtcNow >= _device.DateLastActivity.AddSeconds(60))
+            {
+                try
+                {
+                    // Session is inactive, mark it for Disposal and don't start the elapsed timer.
+                    await _sessionManager.ReportSessionEnded(_session.Id).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error in ReportSessionEnded", ex);
+                }
+            }
         }
         }
 
 
         private string GetServerAddress()
         private string GetServerAddress()
@@ -571,10 +591,21 @@ namespace MediaBrowser.Dlna.PlayTo
                 _device.PlaybackStopped -= _device_PlaybackStopped;
                 _device.PlaybackStopped -= _device_PlaybackStopped;
                 _ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived;
                 _ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived;
 
 
+                DisposeUpdateTimer();
+
                 _device.Dispose();
                 _device.Dispose();
             }
             }
         }
         }
 
 
+        private void DisposeUpdateTimer()
+        {
+            if (_updateTimer != null)
+            {
+                _updateTimer.Dispose();
+                _updateTimer = null;
+            }
+        }
+
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
         public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)

+ 1 - 1
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -64,7 +64,7 @@
     <Compile Include="Subtitles\ISubtitleParser.cs" />
     <Compile Include="Subtitles\ISubtitleParser.cs" />
     <Compile Include="Subtitles\SrtParser.cs" />
     <Compile Include="Subtitles\SrtParser.cs" />
     <Compile Include="Subtitles\SsaParser.cs" />
     <Compile Include="Subtitles\SsaParser.cs" />
-    <Compile Include="Subtitles\SubtitleInfo.cs" />
+    <Compile Include="Subtitles\SubtitleTrackInfo.cs" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">

+ 1 - 1
MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs

@@ -4,6 +4,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
     public interface ISubtitleParser
     public interface ISubtitleParser
     {
     {
-        SubtitleInfo Parse(Stream stream);
+        SubtitleTrackInfo Parse(Stream stream);
     }
     }
 }
 }

+ 2 - 2
MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs

@@ -7,9 +7,9 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
-    public class SrtParser
+    public class SrtParser : ISubtitleParser
     {
     {
-        public SubtitleInfo Parse(Stream stream)
+        public SubtitleTrackInfo Parse(Stream stream)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }

+ 2 - 2
MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs

@@ -7,9 +7,9 @@ using System.Threading.Tasks;
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
-    public class SsaParser
+    public class SsaParser : ISubtitleParser
     {
     {
-        public SubtitleInfo Parse(Stream stream)
+        public SubtitleTrackInfo Parse(Stream stream)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }

+ 2 - 2
MediaBrowser.MediaEncoding/Subtitles/SubtitleInfo.cs → MediaBrowser.MediaEncoding/Subtitles/SubtitleTrackInfo.cs

@@ -2,11 +2,11 @@
 
 
 namespace MediaBrowser.MediaEncoding.Subtitles
 namespace MediaBrowser.MediaEncoding.Subtitles
 {
 {
-    public class SubtitleInfo
+    public class SubtitleTrackInfo
     {
     {
         public List<SubtitleTrackEvent> TrackEvents { get; set; }
         public List<SubtitleTrackEvent> TrackEvents { get; set; }
 
 
-        public SubtitleInfo()
+        public SubtitleTrackInfo()
         {
         {
             TrackEvents = new List<SubtitleTrackEvent>();
             TrackEvents = new List<SubtitleTrackEvent>();
         }
         }

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

@@ -416,6 +416,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
       <Link>Providers\RemoteSearchResult.cs</Link>
       <Link>Providers\RemoteSearchResult.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
+      <Link>Providers\RemoteSubtitleInfo.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
     </Compile>

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

@@ -403,6 +403,9 @@
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
     <Compile Include="..\MediaBrowser.Model\Providers\RemoteSearchResult.cs">
       <Link>Providers\RemoteSearchResult.cs</Link>
       <Link>Providers\RemoteSearchResult.cs</Link>
     </Compile>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Providers\RemoteSubtitleInfo.cs">
+      <Link>Providers\RemoteSubtitleInfo.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
     <Compile Include="..\MediaBrowser.Model\Querying\ArtistsQuery.cs">
       <Link>Querying\ArtistsQuery.cs</Link>
       <Link>Querying\ArtistsQuery.cs</Link>
     </Compile>
     </Compile>

+ 1 - 1
MediaBrowser.Model/ApiClient/IApiClient.cs

@@ -760,7 +760,7 @@ namespace MediaBrowser.Model.ApiClient
         /// </summary>
         /// </summary>
         /// <param name="options">The options.</param>
         /// <param name="options">The options.</param>
         /// <returns>System.String.</returns>
         /// <returns>System.String.</returns>
-        string GetSubtitleUrl(SubtitleOptions options);
+        string GetSubtitleUrl(SubtitleDownloadOptions options);
         
         
         /// <summary>
         /// <summary>
         /// Gets an image url that can be used to download an image from the api
         /// Gets an image url that can be used to download an image from the api

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

@@ -221,6 +221,8 @@ namespace MediaBrowser.Model.Configuration
 
 
         public NotificationOptions NotificationOptions { get; set; }
         public NotificationOptions NotificationOptions { get; set; }
 
 
+        public SubtitleOptions SubtitleOptions { 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>
@@ -284,6 +286,8 @@ namespace MediaBrowser.Model.Configuration
             UICulture = "en-us";
             UICulture = "en-us";
 
 
             NotificationOptions = new NotificationOptions();
             NotificationOptions = new NotificationOptions();
+
+            SubtitleOptions = new SubtitleOptions();
         }
         }
     }
     }
 
 
@@ -311,4 +315,17 @@ namespace MediaBrowser.Model.Configuration
         public string From { get; set; }
         public string From { get; set; }
         public string To { get; set; }
         public string To { get; set; }
     }
     }
+
+    public class SubtitleOptions
+    {
+        public bool RequireExternalSubtitles { get; set; }
+        public string[] SubtitleDownloadLanguages { get; set; }
+        public bool DownloadMovieSubtitles { get; set; }
+        public bool DownloadEpisodeSubtitles { get; set; }
+
+        public SubtitleOptions()
+        {
+            SubtitleDownloadLanguages = new string[] { };
+        }
+    }
 }
 }

+ 1 - 1
MediaBrowser.Model/Dto/StreamOptions.cs

@@ -159,7 +159,7 @@
         public string DeviceId { get; set; }
         public string DeviceId { get; set; }
     }
     }
 
 
-    public class SubtitleOptions
+    public class SubtitleDownloadOptions
     {
     {
         /// <summary>
         /// <summary>
         /// Gets or sets the item identifier.
         /// Gets or sets the item identifier.

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

@@ -139,6 +139,7 @@
     <Compile Include="Notifications\NotificationsSummary.cs" />
     <Compile Include="Notifications\NotificationsSummary.cs" />
     <Compile Include="Providers\RemoteImageResult.cs" />
     <Compile Include="Providers\RemoteImageResult.cs" />
     <Compile Include="Providers\RemoteSearchResult.cs" />
     <Compile Include="Providers\RemoteSearchResult.cs" />
+    <Compile Include="Providers\RemoteSubtitleInfo.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
     <Compile Include="Querying\EpisodeQuery.cs" />
     <Compile Include="Querying\EpisodeQuery.cs" />
     <Compile Include="Querying\ItemCountsQuery.cs" />
     <Compile Include="Querying\ItemCountsQuery.cs" />

+ 19 - 0
MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs

@@ -0,0 +1,19 @@
+using System;
+
+namespace MediaBrowser.Model.Providers
+{
+    public class RemoteSubtitleInfo
+    {
+        public string Language { get; set; }
+        public string Id { get; set; }
+        public string ProviderName { get; set; }
+        public string Name { get; set; }
+        public string Format { get; set; }
+        public string Author { get; set; }
+        public string Comment { get; set; }
+        public DateTime? DateCreated { get; set; }
+        public float? CommunityRating { get; set; }
+        public int? DownloadCount { get; set; }
+        public bool? IsHashMatch { get; set; }
+    }
+}

+ 4 - 0
MediaBrowser.Model/Querying/ItemFilter.cs

@@ -27,6 +27,10 @@ namespace MediaBrowser.Model.Querying
         /// </summary>
         /// </summary>
         IsFavorite = 5,
         IsFavorite = 5,
         /// <summary>
         /// <summary>
+        /// The is recently added
+        /// </summary>
+        IsRecentlyAdded = 6,
+        /// <summary>
         /// The item is resumable
         /// The item is resumable
         /// </summary>
         /// </summary>
         IsResumable = 7,
         IsResumable = 7,

+ 2 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -108,6 +108,7 @@
     <Compile Include="MediaInfo\FFProbeHelpers.cs" />
     <Compile Include="MediaInfo\FFProbeHelpers.cs" />
     <Compile Include="MediaInfo\FFProbeProvider.cs" />
     <Compile Include="MediaInfo\FFProbeProvider.cs" />
     <Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
     <Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
+    <Compile Include="MediaInfo\SubtitleDownloader.cs" />
     <Compile Include="Movies\MovieDbTrailerProvider.cs" />
     <Compile Include="Movies\MovieDbTrailerProvider.cs" />
     <Compile Include="Movies\MovieExternalIds.cs" />
     <Compile Include="Movies\MovieExternalIds.cs" />
     <Compile Include="Movies\TrailerMetadataService.cs" />
     <Compile Include="Movies\TrailerMetadataService.cs" />
@@ -187,6 +188,7 @@
     <Compile Include="Studios\StudiosImageProvider.cs" />
     <Compile Include="Studios\StudiosImageProvider.cs" />
     <Compile Include="Studios\StudioMetadataService.cs" />
     <Compile Include="Studios\StudioMetadataService.cs" />
     <Compile Include="Subtitles\OpenSubtitleDownloader.cs" />
     <Compile Include="Subtitles\OpenSubtitleDownloader.cs" />
+    <Compile Include="Subtitles\SubtitleManager.cs" />
     <Compile Include="TV\EpisodeLocalImageProvider.cs" />
     <Compile Include="TV\EpisodeLocalImageProvider.cs" />
     <Compile Include="TV\EpisodeMetadataService.cs" />
     <Compile Include="TV\EpisodeMetadataService.cs" />
     <Compile Include="TV\EpisodeXmlProvider.cs" />
     <Compile Include="TV\EpisodeXmlProvider.cs" />

+ 10 - 4
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
@@ -10,15 +11,16 @@ using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using System;
 using System;
+using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using System.Linq;
 
 
 namespace MediaBrowser.Providers.MediaInfo
 namespace MediaBrowser.Providers.MediaInfo
 {
 {
@@ -45,6 +47,8 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
         private readonly IEncodingManager _encodingManager;
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _config;
+        private readonly ISubtitleManager _subtitleManager;
 
 
         public string Name
         public string Name
         {
         {
@@ -96,7 +100,7 @@ namespace MediaBrowser.Providers.MediaInfo
             return FetchAudioInfo(item, cancellationToken);
             return FetchAudioInfo(item, cancellationToken);
         }
         }
 
 
-        public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem)
+        public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _isoManager = isoManager;
             _isoManager = isoManager;
@@ -108,6 +112,8 @@ namespace MediaBrowser.Providers.MediaInfo
             _json = json;
             _json = json;
             _encodingManager = encodingManager;
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
+            _config = config;
+            _subtitleManager = subtitleManager;
         }
         }
 
 
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
@@ -134,7 +140,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 return _cachedTask;
                 return _cachedTask;
             }
             }
 
 
-            var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem);
+            var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
 
 
             return prober.ProbeVideo(item, directoryService, cancellationToken);
             return prober.ProbeVideo(item, directoryService, cancellationToken);
         }
         }
@@ -165,7 +171,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 if (video != null && !video.IsPlaceHolder)
                 if (video != null && !video.IsPlaceHolder)
                 {
                 {
-                    var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem);
+                    var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
 
 
                     return !video.SubtitleFiles.SequenceEqual(prober.GetSubtitleFiles(video, directoryService).Select(i => i.FullName).OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
                     return !video.SubtitleFiles.SequenceEqual(prober.GetSubtitleFiles(video, directoryService).Select(i => i.FullName).OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
                 }
                 }

+ 47 - 10
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -2,12 +2,16 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
@@ -35,10 +39,12 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IJsonSerializer _json;
         private readonly IJsonSerializer _json;
         private readonly IEncodingManager _encodingManager;
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
         private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _config;
+        private readonly ISubtitleManager _subtitleManager;
 
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
-        public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem)
+        public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _isoManager = isoManager;
             _isoManager = isoManager;
@@ -50,6 +56,8 @@ namespace MediaBrowser.Providers.MediaInfo
             _json = json;
             _json = json;
             _encodingManager = encodingManager;
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
             _fileSystem = fileSystem;
+            _config = config;
+            _subtitleManager = subtitleManager;
         }
         }
 
 
         public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
         public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -118,7 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
             var idString = item.Id.ToString("N");
             var idString = item.Id.ToString("N");
-            var cachePath = Path.Combine(_appPaths.CachePath, 
+            var cachePath = Path.Combine(_appPaths.CachePath,
                 "ffprobe-video",
                 "ffprobe-video",
                 idString.Substring(0, 2), idString, "v" + SchemaVersion + _mediaEncoder.Version + item.DateModified.Ticks.ToString(_usCulture) + ".json");
                 idString.Substring(0, 2), idString, "v" + SchemaVersion + _mediaEncoder.Version + item.DateModified.Ticks.ToString(_usCulture) + ".json");
 
 
@@ -200,7 +208,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
                 FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
             }
             }
 
 
-            AddExternalSubtitles(video, mediaStreams, directoryService);
+            await AddExternalSubtitles(video, mediaStreams, directoryService, cancellationToken).ConfigureAwait(false);
 
 
             FetchWtvInfo(video, data);
             FetchWtvInfo(video, data);
 
 
@@ -247,7 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
                 }
             }
             }
 
 
-            info.StartPositionTicks = chapter.start/100;
+            info.StartPositionTicks = chapter.start / 100;
 
 
             return info;
             return info;
         }
         }
@@ -450,11 +458,42 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// </summary>
         /// <param name="video">The video.</param>
         /// <param name="video">The video.</param>
         /// <param name="currentStreams">The current streams.</param>
         /// <param name="currentStreams">The current streams.</param>
-        private void AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService)
+        private async Task AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService, CancellationToken cancellationToken)
+        {
+            var externalSubtitleStreams = GetExternalSubtitleStreams(video, currentStreams.Count, directoryService).ToList();
+
+            if ((_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+                video is Episode) ||
+                (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
+                video is Movie))
+            {
+                var downloadedLanguages = await new SubtitleDownloader(_logger,
+                    _subtitleManager)
+                    .DownloadSubtitles(video,
+                    currentStreams,
+                    externalSubtitleStreams,
+                    _config.Configuration.SubtitleOptions.RequireExternalSubtitles,
+                    _config.Configuration.SubtitleOptions.SubtitleDownloadLanguages,
+                    cancellationToken).ConfigureAwait(false);
+
+                // Rescan
+                if (downloadedLanguages.Count > 0)
+                {
+                    externalSubtitleStreams = GetExternalSubtitleStreams(video, currentStreams.Count, directoryService).ToList();
+                }
+            }
+
+            video.SubtitleFiles = externalSubtitleStreams.Select(i => i.Path).OrderBy(i => i).ToList();
+
+            currentStreams.AddRange(externalSubtitleStreams);
+        }
+
+        private IEnumerable<MediaStream> GetExternalSubtitleStreams(Video video, 
+            int startIndex, 
+            IDirectoryService directoryService)
         {
         {
             var files = GetSubtitleFiles(video, directoryService);
             var files = GetSubtitleFiles(video, directoryService);
 
 
-            var startIndex = currentStreams.Count;
             var streams = new List<MediaStream>();
             var streams = new List<MediaStream>();
 
 
             var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
             var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
@@ -504,9 +543,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
                 }
             }
             }
 
 
-            video.SubtitleFiles = streams.Select(i => i.Path).OrderBy(i => i).ToList();
-
-            currentStreams.AddRange(streams);
+            return streams;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -627,7 +664,7 @@ namespace MediaBrowser.Providers.MediaInfo
         {
         {
             var path = mount == null ? item.Path : mount.MountedPath;
             var path = mount == null ? item.Path : mount.MountedPath;
             var dvd = new Dvd(path);
             var dvd = new Dvd(path);
-            
+
             var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
             var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
 
 
             byte? titleNumber = null;
             byte? titleNumber = null;

+ 140 - 0
MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs

@@ -0,0 +1,140 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+    public class SubtitleDownloader
+    {
+        private readonly ILogger _logger;
+        private readonly ISubtitleManager _subtitleManager;
+
+        public SubtitleDownloader(ILogger logger, ISubtitleManager subtitleManager)
+        {
+            _logger = logger;
+            _subtitleManager = subtitleManager;
+        }
+
+        public async Task<List<string>> DownloadSubtitles(Video video,
+            List<MediaStream> internalSubtitleStreams,
+            List<MediaStream> externalSubtitleStreams,
+            bool forceExternal,
+            IEnumerable<string> languages,
+            CancellationToken cancellationToken)
+        {
+            if (video.LocationType != LocationType.FileSystem ||
+                video.VideoType != VideoType.VideoFile)
+            {
+                return new List<string>();
+            }
+
+            SubtitleMediaType mediaType;
+
+            if (video is Episode)
+            {
+                mediaType = SubtitleMediaType.Episode;
+            }
+            else if (video is Movie)
+            {
+                mediaType = SubtitleMediaType.Movie;
+            }
+            else
+            {
+                // These are the only supported types
+                return new List<string>();
+            }
+
+            var downloadedLanguages = new List<string>();
+
+            foreach (var lang in languages)
+            {
+                try
+                {
+                    var downloaded = await DownloadSubtitles(video, internalSubtitleStreams, externalSubtitleStreams, forceExternal, lang, mediaType, cancellationToken)
+                        .ConfigureAwait(false);
+
+                    if (downloaded)
+                    {
+                        downloadedLanguages.Add(lang);
+                    }
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error downloading subtitles", ex);
+                }
+            }
+
+            return downloadedLanguages;
+        }
+
+        private async Task<bool> DownloadSubtitles(Video video,
+            IEnumerable<MediaStream> internalSubtitleStreams,
+            IEnumerable<MediaStream> externalSubtitleStreams,
+            bool forceExternal,
+            string language,
+            SubtitleMediaType mediaType,
+            CancellationToken cancellationToken)
+        {
+            // There's already subtitles for this language
+            if (externalSubtitleStreams.Any(i => string.Equals(i.Language, language, StringComparison.OrdinalIgnoreCase)))
+            {
+                return false;
+            }
+
+            // There's an internal subtitle stream for this language
+            if (!forceExternal && internalSubtitleStreams.Any(i => string.Equals(i.Language, language, StringComparison.OrdinalIgnoreCase)))
+            {
+                return false;
+            }
+
+            var request = new SubtitleSearchRequest
+            {
+                ContentType = mediaType,
+                IndexNumber = video.IndexNumber,
+                Language = language,
+                MediaPath = video.Path,
+                Name = video.Name,
+                ParentIndexNumber = video.ParentIndexNumber,
+                ProductionYear = video.ProductionYear,
+                ProviderIds = video.ProviderIds
+            };
+
+            var episode = video as Episode;
+
+            if (episode != null)
+            {
+                request.IndexNumberEnd = episode.IndexNumberEnd;
+                request.SeriesName = episode.SeriesName;
+            }
+
+            try
+            {
+                var searchResults = await _subtitleManager.SearchSubtitles(request, cancellationToken).ConfigureAwait(false);
+
+                var result = searchResults.FirstOrDefault();
+
+                if (result != null)
+                {
+                    await _subtitleManager.DownloadSubtitles(video, result.Id, result.ProviderName, cancellationToken)
+                            .ConfigureAwait(false);
+
+                    return true;
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error downloading subtitles", ex);
+            }
+
+            return false;
+        }
+    }
+}

+ 81 - 57
MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs

@@ -1,8 +1,10 @@
-using MediaBrowser.Common.Net;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
 using OpenSubtitlesHandler;
 using OpenSubtitlesHandler;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -20,9 +22,9 @@ namespace MediaBrowser.Providers.Subtitles
         private readonly IHttpClient _httpClient;
         private readonly IHttpClient _httpClient;
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
 
-        public OpenSubtitleDownloader(ILogger logger, IHttpClient httpClient)
+        public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient)
         {
         {
-            _logger = logger;
+            _logger = logManager.GetLogger(GetType().Name);
             _httpClient = httpClient;
             _httpClient = httpClient;
         }
         }
 
 
@@ -36,39 +38,71 @@ namespace MediaBrowser.Providers.Subtitles
             get { return new[] { SubtitleMediaType.Episode, SubtitleMediaType.Movie }; }
             get { return new[] { SubtitleMediaType.Episode, SubtitleMediaType.Movie }; }
         }
         }
 
 
-        public Task<SubtitleResponse> GetSubtitles(SubtitleRequest request, CancellationToken cancellationToken)
+        public Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken)
         {
         {
-            return GetSubtitlesInternal(request, cancellationToken);
+            return GetSubtitlesInternal(id, cancellationToken);
         }
         }
 
 
-        private async Task<SubtitleResponse> GetSubtitlesInternal(SubtitleRequest request, 
+        private async Task<SubtitleResponse> GetSubtitlesInternal(string id,
             CancellationToken cancellationToken)
             CancellationToken cancellationToken)
         {
         {
-            var response = new SubtitleResponse();
+            if (string.IsNullOrWhiteSpace(id))
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            var idParts = id.Split(new[] { '-' }, 3);
+
+            var format = idParts[0];
+            var language = idParts[1];
+            var ossId = idParts[2];
+
+            var downloadsList = new[] { int.Parse(ossId, _usCulture) };
+
+            var resultDownLoad = OpenSubtitles.DownloadSubtitles(downloadsList);
+            if (!(resultDownLoad is MethodResponseSubtitleDownload))
+            {
+                throw new ApplicationException("Invalid response type");
+            }
 
 
+            var res = ((MethodResponseSubtitleDownload)resultDownLoad).Results.First();
+            var data = Convert.FromBase64String(res.Data);
+
+            return new SubtitleResponse
+            {
+                Format = format,
+                Language = language,
+
+                Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data)))
+            };
+        }
+
+        public async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
+        {
             var imdbIdText = request.GetProviderId(MetadataProviders.Imdb);
             var imdbIdText = request.GetProviderId(MetadataProviders.Imdb);
             long imdbId;
             long imdbId;
 
 
             if (string.IsNullOrWhiteSpace(imdbIdText) ||
             if (string.IsNullOrWhiteSpace(imdbIdText) ||
-                long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId))
+                !long.TryParse(imdbIdText.TrimStart('t'), NumberStyles.Any, _usCulture, out imdbId))
             {
             {
-                return response;
+                _logger.Debug("Imdb id missing");
+                return new List<RemoteSubtitleInfo>();
             }
             }
-            
+
             switch (request.ContentType)
             switch (request.ContentType)
             {
             {
                 case SubtitleMediaType.Episode:
                 case SubtitleMediaType.Episode:
                     if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName))
                     if (!request.IndexNumber.HasValue || !request.ParentIndexNumber.HasValue || string.IsNullOrEmpty(request.SeriesName))
                     {
                     {
-                        _logger.Debug("Information Missing");
-                        return response;
+                        _logger.Debug("Episode information missing");
+                        return new List<RemoteSubtitleInfo>();
                     }
                     }
                     break;
                     break;
                 case SubtitleMediaType.Movie:
                 case SubtitleMediaType.Movie:
                     if (string.IsNullOrEmpty(request.Name))
                     if (string.IsNullOrEmpty(request.Name))
                     {
                     {
-                        _logger.Debug("Information Missing");
-                        return response;
+                        _logger.Debug("Movie name missing");
+                        return new List<RemoteSubtitleInfo>();
                     }
                     }
                     break;
                     break;
             }
             }
@@ -76,16 +110,18 @@ namespace MediaBrowser.Providers.Subtitles
             if (string.IsNullOrEmpty(request.MediaPath))
             if (string.IsNullOrEmpty(request.MediaPath))
             {
             {
                 _logger.Debug("Path Missing");
                 _logger.Debug("Path Missing");
-                return response;
+                return new List<RemoteSubtitleInfo>();
             }
             }
 
 
             Utilities.HttpClient = _httpClient;
             Utilities.HttpClient = _httpClient;
             OpenSubtitles.SetUserAgent("OS Test User Agent");
             OpenSubtitles.SetUserAgent("OS Test User Agent");
-            var loginResponse = OpenSubtitles.LogIn("", "", "en");
+
+            var loginResponse = await OpenSubtitles.LogInAsync("", "", "en", cancellationToken).ConfigureAwait(false);
+
             if (!(loginResponse is MethodResponseLogIn))
             if (!(loginResponse is MethodResponseLogIn))
             {
             {
                 _logger.Debug("Login error");
                 _logger.Debug("Login error");
-                return response;
+                return new List<RemoteSubtitleInfo>();
             }
             }
 
 
             var subLanguageId = request.Language;
             var subLanguageId = request.Language;
@@ -105,54 +141,42 @@ namespace MediaBrowser.Providers.Subtitles
             var result = OpenSubtitles.SearchSubtitles(parms.ToArray());
             var result = OpenSubtitles.SearchSubtitles(parms.ToArray());
             if (!(result is MethodResponseSubtitleSearch))
             if (!(result is MethodResponseSubtitleSearch))
             {
             {
-                _logger.Debug("invalid response type");
-                return null;
+                _logger.Debug("Invalid response type");
+                return new List<RemoteSubtitleInfo>();
             }
             }
 
 
             Predicate<SubtitleSearchResult> mediaFilter =
             Predicate<SubtitleSearchResult> mediaFilter =
                 x =>
                 x =>
                     request.ContentType == SubtitleMediaType.Episode
                     request.ContentType == SubtitleMediaType.Episode
-                        ? int.Parse(x.SeriesSeason) == request.ParentIndexNumber && int.Parse(x.SeriesEpisode) == request.IndexNumber
-                        : long.Parse(x.IDMovieImdb) == imdbId;
+                        ? int.Parse(x.SeriesSeason, _usCulture) == request.ParentIndexNumber && int.Parse(x.SeriesEpisode, _usCulture) == request.IndexNumber
+                        : long.Parse(x.IDMovieImdb, _usCulture) == imdbId;
 
 
             var results = ((MethodResponseSubtitleSearch)result).Results;
             var results = ((MethodResponseSubtitleSearch)result).Results;
-            var bestResult = results.Where(x => x.SubBad == "0" && mediaFilter(x))
-                    .OrderBy(x => x.MovieHash == hash)
-                    .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize) - movieByteSize))
-                    .ThenByDescending(x => int.Parse(x.SubDownloadsCnt))
-                    .ThenByDescending(x => double.Parse(x.SubRating))
-                    .ToList();
-
-            if (!bestResult.Any())
-            {
-                _logger.Debug("No Subtitles");
-                return response;
-            }
-
-            _logger.Debug("Found " + bestResult.Count + " subtitles.");
 
 
-            var subtitle = bestResult.First();
-            var downloadsList = new[] { int.Parse(subtitle.IDSubtitleFile) };
+            // Avoid implicitly captured closure
+            var hasCopy = hash;
 
 
-            var resultDownLoad = OpenSubtitles.DownloadSubtitles(downloadsList);
-            if (!(resultDownLoad is MethodResponseSubtitleDownload))
-            {
-                _logger.Debug("invalid response type");
-                return response;
-            }
-            if (!((MethodResponseSubtitleDownload)resultDownLoad).Results.Any())
-            {
-                _logger.Debug("No Subtitle Downloads");
-                return response;
-            }
-
-            var res = ((MethodResponseSubtitleDownload)resultDownLoad).Results.First();
-            var data = Convert.FromBase64String(res.Data);
-
-            response.HasContent = true;
-            response.Format = subtitle.SubFormat.ToUpper();
-            response.Stream = new MemoryStream(Utilities.Decompress(new MemoryStream(data)));
-            return response;
+            return results.Where(x => x.SubBad == "0" && mediaFilter(x))
+                    .OrderBy(x => x.MovieHash == hash)
+                    .ThenBy(x => Math.Abs(long.Parse(x.MovieByteSize, _usCulture) - movieByteSize))
+                    .ThenByDescending(x => int.Parse(x.SubDownloadsCnt, _usCulture))
+                    .ThenByDescending(x => double.Parse(x.SubRating, _usCulture))
+                    .Select(i => new RemoteSubtitleInfo
+                    {
+                        Author = i.UserNickName,
+                        Comment = i.SubAuthorComment,
+                        CommunityRating = float.Parse(i.SubRating, _usCulture),
+                        DownloadCount = int.Parse(i.SubDownloadsCnt, _usCulture),
+                        Format = i.SubFormat,
+                        ProviderName = Name,
+                        Language = i.SubLanguageID,
+
+                        Id = i.SubFormat + "-" + i.SubLanguageID + "-" + i.IDSubtitle,
+
+                        Name = i.SubFileName,
+                        DateCreated = DateTime.Parse(i.SubAddDate, _usCulture),
+                        IsHashMatch = i.MovieHash == hasCopy
+                    });
         }
         }
     }
     }
 }
 }

+ 141 - 0
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -0,0 +1,141 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Providers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Subtitles
+{
+    public class SubtitleManager : ISubtitleManager
+    {
+        private ISubtitleProvider[] _subtitleProviders;
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+        private readonly ILibraryMonitor _monitor;
+
+        public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor)
+        {
+            _logger = logger;
+            _fileSystem = fileSystem;
+            _monitor = monitor;
+        }
+
+        public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders)
+        {
+            _subtitleProviders = subtitleProviders.ToArray();
+        }
+
+        public async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
+        {
+            var providers = _subtitleProviders
+                .Where(i => i.SupportedMediaTypes.Contains(request.ContentType))
+                .ToList();
+
+            var tasks = providers.Select(async i =>
+            {
+                try
+                {
+                    return await i.SearchSubtitles(request, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error downloading subtitles from {0}", ex, i.Name);
+                    return new List<RemoteSubtitleInfo>();
+                }
+            });
+
+            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            return results.SelectMany(i => i);
+        }
+
+        public async Task DownloadSubtitles(Video video,
+            string subtitleId,
+            string providerName,
+            CancellationToken cancellationToken)
+        {
+            var provider = _subtitleProviders.First(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase));
+
+            var response = await provider.GetSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
+
+            using (var stream = response.Stream)
+            {
+                var savePath = Path.Combine(Path.GetDirectoryName(video.Path), 
+                    Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower() + "." + response.Format.ToLower());
+
+                _logger.Info("Saving subtitles to {0}", savePath);
+
+                _monitor.ReportFileSystemChangeBeginning(savePath);
+
+                try
+                {
+                    using (var fs = _fileSystem.GetFileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, true))
+                    {
+                        await stream.CopyToAsync(fs).ConfigureAwait(false);
+                    }
+                }
+                finally
+                {
+                    _monitor.ReportFileSystemChangeComplete(savePath, false);
+                }
+            }
+        }
+
+        public Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(Video video, string language, CancellationToken cancellationToken)
+        {
+            if (video.LocationType != LocationType.FileSystem ||
+                video.VideoType != VideoType.VideoFile)
+            {
+                return Task.FromResult<IEnumerable<RemoteSubtitleInfo>>(new List<RemoteSubtitleInfo>());
+            }
+
+            SubtitleMediaType mediaType;
+
+            if (video is Episode)
+            {
+                mediaType = SubtitleMediaType.Episode;
+            }
+            else if (video is Movie)
+            {
+                mediaType = SubtitleMediaType.Movie;
+            }
+            else
+            {
+                // These are the only supported types
+                return Task.FromResult<IEnumerable<RemoteSubtitleInfo>>(new List<RemoteSubtitleInfo>());
+            }
+
+            var request = new SubtitleSearchRequest
+            {
+                ContentType = mediaType,
+                IndexNumber = video.IndexNumber,
+                Language = language,
+                MediaPath = video.Path,
+                Name = video.Name,
+                ParentIndexNumber = video.ParentIndexNumber,
+                ProductionYear = video.ProductionYear,
+                ProviderIds = video.ProviderIds
+            };
+
+            var episode = video as Episode;
+
+            if (episode != null)
+            {
+                request.IndexNumberEnd = episode.IndexNumberEnd;
+                request.SeriesName = episode.SeriesName;
+            }
+
+            return SearchSubtitles(request, cancellationToken);
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Server.Implementations/Channels/ChannelManager.cs

@@ -327,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Channels
 
 
             var categoryKey = string.IsNullOrWhiteSpace(categoryId) ? "root" : categoryId.GetMD5().ToString("N");
             var categoryKey = string.IsNullOrWhiteSpace(categoryId) ? "root" : categoryId.GetMD5().ToString("N");
 
 
-            return Path.Combine(_config.ApplicationPaths.CachePath, channelId, categoryKey, user.Id.ToString("N") + ".json");
+            return Path.Combine(_config.ApplicationPaths.CachePath, "channels", channelId, categoryKey, user.Id.ToString("N") + ".json");
         }
         }
 
 
         private async Task<QueryResult<BaseItemDto>> GetReturnItems(IEnumerable<BaseItem> items, User user, ChannelItemQuery query, CancellationToken cancellationToken)
         private async Task<QueryResult<BaseItemDto>> GetReturnItems(IEnumerable<BaseItem> items, User user, ChannelItemQuery query, CancellationToken cancellationToken)

+ 7 - 1
MediaBrowser.Server.Implementations/Collections/CollectionManager.cs

@@ -93,7 +93,13 @@ namespace MediaBrowser.Server.Implementations.Collections
                 // Find an actual physical folder
                 // Find an actual physical folder
                 if (folder is CollectionFolder)
                 if (folder is CollectionFolder)
                 {
                 {
-                    return _libraryManager.RootFolder.Children.OfType<Folder>().First(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase));
+                    var child = _libraryManager.RootFolder.Children.OfType<Folder>()
+                        .FirstOrDefault(i => folder.PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase));
+
+                    if (child != null)
+                    {
+                        return child;
+                    }
                 }
                 }
             }
             }
 
 

+ 4 - 2
MediaBrowser.Server.Implementations/HttpServer/ServerLogger.cs

@@ -206,7 +206,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <param name="message">The message.</param>
         /// <param name="message">The message.</param>
         public void Warn(object message)
         public void Warn(object message)
         {
         {
-            _logger.Warn(GetMesssage(message));
+            // Hide StringMapTypeDeserializer messages
+            // _logger.Warn(GetMesssage(message));
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -216,7 +217,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <param name="args">The args.</param>
         /// <param name="args">The args.</param>
         public void WarnFormat(string format, params object[] args)
         public void WarnFormat(string format, params object[] args)
         {
         {
-            _logger.Warn(format, args);
+            // Hide StringMapTypeDeserializer messages
+            // _logger.Warn(format, args);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 0 - 44
MediaBrowser.Server.Implementations/Library/Validators/PeoplePostScanTask.cs

@@ -1,44 +0,0 @@
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.Library.Validators
-{
-    class PeoplePostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The _library manager
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// The _logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        public PeoplePostScanTask(ILibraryManager libraryManager, ILogger logger)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-        }
-
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            return new PeopleValidator(_libraryManager, _logger).ValidatePeople(cancellationToken, new MetadataRefreshOptions
-            {
-                ImageRefreshMode = ImageRefreshMode.ValidationOnly,
-                MetadataRefreshMode = MetadataRefreshMode.None
-
-            }, progress);
-        }
-    }
-}

+ 14 - 33
MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Localization;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
-using MoreLinq;
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -106,16 +105,13 @@ namespace MediaBrowser.Server.Implementations.Localization
         /// <returns>IEnumerable{CultureDto}.</returns>
         /// <returns>IEnumerable{CultureDto}.</returns>
         public IEnumerable<CultureDto> GetCultures()
         public IEnumerable<CultureDto> GetCultures()
         {
         {
-            return CultureInfo.GetCultures(CultureTypes.AllCultures)
-                .OrderBy(c => c.DisplayName)
-                .DistinctBy(c => c.TwoLetterISOLanguageName + c.ThreeLetterISOLanguageName)
-                .Select(c => new CultureDto
-                {
-                    Name = c.Name,
-                    DisplayName = c.DisplayName,
-                    ThreeLetterISOLanguageName = c.ThreeLetterISOLanguageName,
-                    TwoLetterISOLanguageName = c.TwoLetterISOLanguageName
-                });
+            var type = GetType();
+            var path = type.Namespace + ".cultures.json";
+
+            using (var stream = type.Assembly.GetManifestResourceStream(path))
+            {
+                return _jsonSerializer.DeserializeFromStream<List<CultureDto>>(stream);
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -124,28 +120,13 @@ namespace MediaBrowser.Server.Implementations.Localization
         /// <returns>IEnumerable{CountryInfo}.</returns>
         /// <returns>IEnumerable{CountryInfo}.</returns>
         public IEnumerable<CountryInfo> GetCountries()
         public IEnumerable<CountryInfo> GetCountries()
         {
         {
-            return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
-                .Select(c =>
-                {
-                    try
-                    {
-                        return new RegionInfo(c.LCID);
-                    }
-                    catch (CultureNotFoundException)
-                    {
-                        return null;
-                    }
-                })
-                .Where(i => i != null)
-                .OrderBy(c => c.DisplayName)
-                .DistinctBy(c => c.TwoLetterISORegionName)
-                .Select(c => new CountryInfo
-                {
-                    Name = c.Name,
-                    DisplayName = c.DisplayName,
-                    TwoLetterISORegionName = c.TwoLetterISORegionName,
-                    ThreeLetterISORegionName = c.ThreeLetterISORegionName
-                });
+            var type = GetType();
+            var path = type.Namespace + ".countries.json";
+
+            using (var stream = type.Assembly.GetManifestResourceStream(path))
+            {
+                return _jsonSerializer.DeserializeFromStream<List<CountryInfo>>(stream);
+            }
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 80 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -627,5 +627,84 @@
 	"OptionSpecialFeatures": "Special Features",
 	"OptionSpecialFeatures": "Special Features",
 	"HeaderCollections": "Collections",
 	"HeaderCollections": "Collections",
 	"HeaderChannels": "Channels",
 	"HeaderChannels": "Channels",
-	"HeaderMyLibrary": "My Library"
+	"HeaderMyLibrary": "My Library",
+	"LabelProfileCodecsHelp": "Separated by comma. This can be left empty to apply to all codecs.",
+	"LabelProfileContainersHelp": "Separated by comma. This can be left empty to apply to all containers.",
+	"HeaderResponseProfile": "Response Profile",
+	"LabelType": "Type:",
+	"LabelProfileContainer": "Container:",
+	"LabelProfileVideoCodecs": "Video codecs:",
+	"LabelProfileAudioCodecs": "Audio codecs:",
+	"LabelProfileCodecs": "Codecs:",
+	"HeaderDirectPlayProfile": "Direct Play Profile",
+	"HeaderTranscodingProfile": "Transcoding Profile",
+	"HeaderCodecProfile": "Codec Profile",
+	"HeaderCodecProfileHelp": "Define additional conditions that must be met in order for a codec to be direct played.",
+	"HeaderContainerProfile": "Container Profile",
+	"HeaderContainerProfileHelp": "Define additional conditions that must be met in order for a file to be direct played.",
+	"OptionProfileVideo": "Video",
+	"OptionProfileAudio": "Audio",
+	"OptionProfileVideoAudio": "Video Audio",
+	"OptionProfilePhoto": "Photo",
+	"LabelUserLibrary": "User library:",
+	"LabelUserLibraryHelp": "Select which user library to display to the device. Leave empty to inherit the default setting.",
+	"OptionPlainStorageFolders": "Display all folders as plain storage folders",
+	"OptionPlainStorageFoldersHelp": "If enabled, all folders are represented in DIDL as \"object.container.storageFolder\" instead of a more specific type, such as \"object.container.person.musicArtist\".",
+	"OptionPlainVideoItems": "Display all videos as plain video items",
+	"OptionPlainVideoItemsHelp": "If enabled, all videos are represented in DIDL as \"object.item.videoItem\" instead of a more specific type, such as \"object.item.videoItem.movie\".",
+	"LabelSupportedMediaTypes": "Supported Media Types:",
+	"TabIdentification": "Identification",
+	"TabDirectPlay": "Direct Play",
+	"TabContainers": "Containers",
+	"TabCodecs": "Codecs",
+	"TabResponses": "Responses",
+	"HeaderProfileInformation": "Profile Information",
+	"LabelEmbedAlbumArtDidl": "Embed album art in Didl",
+	"LabelEmbedAlbumArtDidlHelp": "Some devices prefer this method for obtaining album art. Others may fail to play with this option enabled.",
+	"LabelAlbumArtPN": "Album art PN:",
+	"LabelAlbumArtHelp": "PN used for album art, within the dlna:profileID attribute on upnp:albumArtURI. Some clients require a specific value, regardless of the size of the image.",
+	"LabelAlbumArtMaxWidth": "Album art max width:",
+	"LabelAlbumArtMaxWidthHelp": "Max resolution of album art exposed via upnp:albumArtURI.",
+	"LabelAlbumArtMaxHeight": "Album art max height:",
+	"LabelAlbumArtMaxHeightHelp": "Max resolution of album art exposed via upnp:albumArtURI.",
+	"LabelIconMaxWidth": "Icon max width:",
+	"LabelIconMaxWidthHelp": "Max resolution of icons exposed via upnp:icon.",
+	"LabelIconMaxHeight": "Icon max height:",
+	"LabelIconMaxHeightHelp": "Max resolution of icons exposed via upnp:icon.",
+	"LabelIdentificationFieldHelp": "A case-insensitive substring or regex expression.",
+	"HeaderProfileServerSettingsHelp": "These values control how Media Browser will present itself to the device.",
+	"LabelMaxBitrate": "Max bitrate:",
+	"LabelMaxBitrateHelp": "Specify a max bitrate in bandwidth constrained environments, or if the device imposes it's own limit.",
+	"OptionIgnoreTranscodeByteRangeRequests": "Ignore transcode byte range requests",
+	"OptionIgnoreTranscodeByteRangeRequestsHelp": "If enabled, these requests will be honored but will ignore the byte range header.",
+	"LabelFriendlyName": "Friendly name",
+	"LabelManufacturer": "Manufacturer",
+	"LabelManufacturerUrl": "Manufacturer url",
+	"LabelModelName": "Model name",
+	"LabelModelNumber": "Model number",
+	"LabelModelDescription": "Model description",
+	"LabelModelUrl": "Model url",
+	"LabelSerialNumber": "Serial number",
+	"LabelDeviceDescription": "Device description",
+	"HeaderIdentificationCriteriaHelp": "Enter at least one identification criteria.",
+	"HeaderDirectPlayProfileHelp": "Add direct play profiles to indicate which formats the device can handle natively.",
+	"HeaderTranscodingProfileHelp": "Add transcoding profiles to indicate which formats should be used when transcoding is required.",
+	"HeaderContainerProfileHelp": "Container profiles indicate the limitations of a device when playing specific formats. If a limitation applies then the media will be transcoded, even if the format is configured for direct play.",
+	"HeaderCodecProfileHelp": "Codec profiles indicate the limitations of a device when playing specific codecs. If a limitation applies then the media will be transcoded, even if the codec is configured for direct play.",
+	"HeaderResponseProfileHelp": "Response profiles provide a way to customize information sent to the device when playing certain kinds of media.",
+	"LabelXDlnaCap": "X-Dlna cap:",
+	"LabelXDlnaCapHelp": "Determines the content of the X_DLNACAP element in the urn:schemas-dlna-org:device-1-0 namespace.",
+	"LabelXDlnaDoc": "X-Dlna doc:",
+	"LabelXDlnaDocHelp": "Determines the content of the X_DLNADOC element in the urn:schemas-dlna-org:device-1-0 namespace.",
+	"LabelSonyAggregationFlags": "Sony aggregation flags:",
+	"LabelSonyAggregationFlagsHelp": "Determines the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace.",
+	"LabelTranscodingContainer": "Container:",
+	"LabelTranscodingVideoCodec": "Video codec:",
+	"LabelTranscodingVideoProfile": "Video profile:",
+	"LabelTranscodingAudioCodec": "Audio codec:",
+	"OptionEnableM2tsMode": "Enable M2ts mode",
+	"OptionEnableM2tsModeHelp": "Enable m2ts mode when encoding to mpegts.",
+	"OptionEstimateContentLength": "Estimate content length when transcoding",
+	"OptionReportByteRangeSeekingWhenTranscoding": "Report that the server supports byte seeking when transcoding",
+	"OptionReportByteRangeSeekingWhenTranscodingHelp": "This is required for some devices that don't time seek very well."
 }
 }

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/countries.json


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/cultures.json


+ 2 - 1
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -169,7 +169,6 @@
     <Compile Include="Library\Validators\GenresValidator.cs" />
     <Compile Include="Library\Validators\GenresValidator.cs" />
     <Compile Include="Library\Validators\MusicGenresPostScanTask.cs" />
     <Compile Include="Library\Validators\MusicGenresPostScanTask.cs" />
     <Compile Include="Library\Validators\MusicGenresValidator.cs" />
     <Compile Include="Library\Validators\MusicGenresValidator.cs" />
-    <Compile Include="Library\Validators\PeoplePostScanTask.cs" />
     <Compile Include="Library\Validators\PeopleValidator.cs" />
     <Compile Include="Library\Validators\PeopleValidator.cs" />
     <Compile Include="Library\Validators\StudiosPostScanTask.cs" />
     <Compile Include="Library\Validators\StudiosPostScanTask.cs" />
     <Compile Include="Library\Validators\StudiosValidator.cs" />
     <Compile Include="Library\Validators\StudiosValidator.cs" />
@@ -328,6 +327,8 @@
     <EmbeddedResource Include="Localization\Server\ms.json" />
     <EmbeddedResource Include="Localization\Server\ms.json" />
     <EmbeddedResource Include="Localization\JavaScript\kk.json" />
     <EmbeddedResource Include="Localization\JavaScript\kk.json" />
     <EmbeddedResource Include="Localization\Server\kk.json" />
     <EmbeddedResource Include="Localization\Server\kk.json" />
+    <EmbeddedResource Include="Localization\countries.json" />
+    <EmbeddedResource Include="Localization\cultures.json" />
     <None Include="packages.config" />
     <None Include="packages.config" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>

+ 9 - 2
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -31,11 +31,11 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Themes;
 using MediaBrowser.Controller.Themes;
 using MediaBrowser.Dlna;
 using MediaBrowser.Dlna;
 using MediaBrowser.Dlna.Eventing;
 using MediaBrowser.Dlna.Eventing;
 using MediaBrowser.Dlna.Main;
 using MediaBrowser.Dlna.Main;
-using MediaBrowser.Dlna.PlayTo;
 using MediaBrowser.Dlna.Server;
 using MediaBrowser.Dlna.Server;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Encoder;
 using MediaBrowser.MediaEncoding.Encoder;
@@ -44,6 +44,7 @@ using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Server.Implementations;
 using MediaBrowser.Server.Implementations;
 using MediaBrowser.Server.Implementations.Channels;
 using MediaBrowser.Server.Implementations.Channels;
 using MediaBrowser.Server.Implementations.Collections;
 using MediaBrowser.Server.Implementations.Collections;
@@ -193,6 +194,7 @@ namespace MediaBrowser.ServerApplication
         private IProviderRepository ProviderRepository { get; set; }
         private IProviderRepository ProviderRepository { get; set; }
 
 
         private INotificationManager NotificationManager { get; set; }
         private INotificationManager NotificationManager { get; set; }
+        private ISubtitleManager SubtitleManager { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -531,6 +533,9 @@ namespace MediaBrowser.ServerApplication
             NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager);
             NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager);
             RegisterSingleInstance(NotificationManager);
             RegisterSingleInstance(NotificationManager);
 
 
+            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor);
+            RegisterSingleInstance(SubtitleManager);
+
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
             var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false));
             var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false));
             var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false));
             var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false));
@@ -566,7 +571,7 @@ namespace MediaBrowser.ServerApplication
         {
         {
             var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo(progress).ConfigureAwait(false);
             var info = await new FFMpegDownloader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager).GetFFMpegInfo(progress).ConfigureAwait(false);
 
 
-            MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.Path, info.ProbePath, info.Version, FileSystemManager);
+            MediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), ApplicationPaths, JsonSerializer, info.EncoderPath, info.ProbePath, info.Version, FileSystemManager);
             RegisterSingleInstance(MediaEncoder);
             RegisterSingleInstance(MediaEncoder);
         }
         }
 
 
@@ -710,6 +715,8 @@ namespace MediaBrowser.ServerApplication
 
 
             LiveTvManager.AddParts(GetExports<ILiveTvService>());
             LiveTvManager.AddParts(GetExports<ILiveTvService>());
 
 
+            SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
+            
             SessionManager.AddParts(GetExports<ISessionControllerFactory>());
             SessionManager.AddParts(GetExports<ISessionControllerFactory>());
 
 
             ChannelManager.AddParts(GetExports<IChannel>(), GetExports<IChannelFactory>());
             ChannelManager.AddParts(GetExports<IChannel>(), GetExports<IChannelFactory>());

+ 48 - 33
MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloadInfo.cs

@@ -4,6 +4,8 @@ using Mono.Unix.Native;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using System.IO;
 using System.IO;
 #endif
 #endif
+using System.IO;
+using System.Text.RegularExpressions;
 
 
 namespace MediaBrowser.ServerApplication.FFMpeg
 namespace MediaBrowser.ServerApplication.FFMpeg
 {
 {
@@ -32,7 +34,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     switch (arg)
                     switch (arg)
                     {
                     {
                         case "Version":
                         case "Version":
-                            return "20140304";
+                            return "20140506";
                         case "FFMpegFilename":
                         case "FFMpegFilename":
                             return "ffmpeg.exe";
                             return "ffmpeg.exe";
                         case "FFProbeFilename":
                         case "FFProbeFilename":
@@ -42,7 +44,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     }
                     }
                     break;
                     break;
 
 
-                #if __MonoCS__
                 case PlatformID.Unix:
                 case PlatformID.Unix:
                     if (PlatformDetection.IsMac)
                     if (PlatformDetection.IsMac)
                     {
                     {
@@ -69,7 +70,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                             switch (arg)
                             switch (arg)
                             {
                             {
                                 case "Version":
                                 case "Version":
-                                    return "20140304";
+                                    return "20140506";
                                 case "FFMpegFilename":
                                 case "FFMpegFilename":
                                     return "ffmpeg";
                                     return "ffmpeg";
                                 case "FFProbeFilename":
                                 case "FFProbeFilename":
@@ -85,7 +86,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                             switch (arg)
                             switch (arg)
                             {
                             {
                                 case "Version":
                                 case "Version":
-                                    return "20140304";
+                                    return "20140505";
                                 case "FFMpegFilename":
                                 case "FFMpegFilename":
                                     return "ffmpeg";
                                     return "ffmpeg";
                                 case "FFProbeFilename":
                                 case "FFProbeFilename":
@@ -98,7 +99,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     }
                     }
                     // Unsupported Unix platform
                     // Unsupported Unix platform
                     return "";
                     return "";
-#endif
             }
             }
             return "";
             return "";
         }
         }
@@ -106,18 +106,17 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         private static string[] GetDownloadUrls()
         private static string[] GetDownloadUrls()
         {
         {
             var pid = Environment.OSVersion.Platform;
             var pid = Environment.OSVersion.Platform;
-            
+
             switch (pid)
             switch (pid)
             {
             {
                 case PlatformID.Win32NT:
                 case PlatformID.Win32NT:
                     return new[]
                     return new[]
                     {
                     {
-                        "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20140304-git-f34cceb-win32-static.7z",
-                        "https://www.dropbox.com/s/6brdetuzbld93jk/ffmpeg-20140304-git-f34cceb-win32-static.7z?dl=1"
+                        "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20140506-git-2baf1c8-win32-static.7z",
+                        "https://www.dropbox.com/s/lxlzxs0r83iatsv/ffmpeg-20140506-git-2baf1c8-win32-static.7z?dl=1"
                     };
                     };
-           
-                    #if __MonoCS__
-                case PlatformID.Unix: 
+
+                case PlatformID.Unix:
                     if (PlatformDetection.IsMac && PlatformDetection.IsX86_64)
                     if (PlatformDetection.IsMac && PlatformDetection.IsX86_64)
                     {
                     {
                         return new[]
                         return new[]
@@ -132,8 +131,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                         {
                         {
                             return new[]
                             return new[]
                             {
                             {
-                                "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.2014-03-04.tar.gz",
-                                "https://www.dropbox.com/s/0l76mcauqqkta31/ffmpeg.static.32bit.2014-03-04.tar.gz?dl=1"
+                                "http://ffmpeg.gusari.org/static/32bit/ffmpeg.static.32bit.latest.tar.gz",
+                                "https://www.dropbox.com/s/k9s02pv5to6slfb/ffmpeg.static.32bit.2014-05-06.tar.gz?dl=1"
                             };
                             };
                         }
                         }
 
 
@@ -141,22 +140,20 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                         {
                         {
                             return new[]
                             return new[]
                             {
                             {
-                                "http://ffmpeg.gusari.org/static/64bit/ffmpeg.static.64bit.2014-03-04.tar.gz",
-                                "https://www.dropbox.com/s/9wlxz440mdejuqe/ffmpeg.static.64bit.2014-03-04.tar.gz?dl=1"
+                                "http://ffmpeg.gusari.org/static/64bit/ffmpeg.static.64bit.latest.tar.gz",
+                                "https://www.dropbox.com/s/onuregwghywnzjo/ffmpeg.static.64bit.2014-05-05.tar.gz?dl=1"
                             };
                             };
                         }
                         }
 
 
                     }
                     }
 
 
                     //No Unix version available 
                     //No Unix version available 
-                    return new string[] {};
-#endif
+                    return new string[] { };
             }
             }
-            return new string[] {};
+            return new string[] { };
         }
         }
     }
     }
 
 
-    #if __MonoCS__
     public static class PlatformDetection
     public static class PlatformDetection
     {
     {
         public readonly static bool IsWindows;
         public readonly static bool IsWindows;
@@ -166,34 +163,52 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         public readonly static bool IsX86_64;
         public readonly static bool IsX86_64;
         public readonly static bool IsArm;
         public readonly static bool IsArm;
 
 
-        static PlatformDetection ()
+        static PlatformDetection()
         {
         {
             IsWindows = Path.DirectorySeparatorChar == '\\';
             IsWindows = Path.DirectorySeparatorChar == '\\';
 
 
             //Don't call uname on windows
             //Don't call uname on windows
             if (!IsWindows)
             if (!IsWindows)
             {
             {
-                Utsname uname;
-                var callResult = Syscall.uname(out uname);
-                if (callResult == 0)
-                {
-                    IsMac = uname.sysname == "Darwin";
-                    IsLinux = !IsMac && uname.sysname == "Linux";
+                var uname = GetUnixName();
 
 
-                    Regex archX86 = new Regex("(i|I)[3-6]86");
-                    IsX86 = archX86.IsMatch(uname.machine);
-                    IsX86_64 = !IsX86 && uname.machine == "x86_64";
-                    IsArm = !IsX86 && !IsX86 && uname.machine.StartsWith("arm");
-                }
+                IsMac = uname.sysname == "Darwin";
+                IsLinux = uname.sysname == "Linux";
+
+                var archX86 = new Regex("(i|I)[3-6]86");
+                IsX86 = archX86.IsMatch(uname.machine);
+                IsX86_64 = !IsX86 && uname.machine == "x86_64";
+                IsArm = !IsX86 && !IsX86_64 && uname.machine.StartsWith("arm");
             }
             }
             else
             else
             {
             {
-                if (System.Environment.Is64BitOperatingSystem)
+                if (Environment.Is64BitOperatingSystem)
                     IsX86_64 = true;
                     IsX86_64 = true;
                 else
                 else
                     IsX86 = true;
                     IsX86 = true;
             }
             }
         }
         }
+
+        private static Uname GetUnixName()
+        {
+            var uname = new Uname();
+
+#if __MonoCS__
+                Utsname uname;
+                var callResult = Syscall.uname(out uname);
+                if (callResult == 0)
+                {
+                    uname.sysname= uname.sysname;
+                    uname.machine= uname.machine;
+                }
+#endif
+            return uname;
+        }
+    }
+
+    public class Uname
+    {
+        public string sysname = string.Empty;
+        public string machine = string.Empty;
     }
     }
-    #endif
 }
 }

+ 77 - 58
MediaBrowser.ServerApplication/FFMpeg/FFMpegDownloader.cs

@@ -42,63 +42,86 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
 
         public async Task<FFMpegInfo> GetFFMpegInfo(IProgress<double> progress)
         public async Task<FFMpegInfo> GetFFMpegInfo(IProgress<double> progress)
         {
         {
-            var versionedDirectoryPath = Path.Combine(GetMediaToolsPath(true), FFMpegDownloadInfo.Version);
+            var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
+            var versionedDirectoryPath = Path.Combine(rootEncoderPath, FFMpegDownloadInfo.Version);
 
 
             var info = new FFMpegInfo
             var info = new FFMpegInfo
             {
             {
                 ProbePath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFProbeFilename),
                 ProbePath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFProbeFilename),
-                Path = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
+                EncoderPath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
                 Version = FFMpegDownloadInfo.Version
                 Version = FFMpegDownloadInfo.Version
             };
             };
 
 
             Directory.CreateDirectory(versionedDirectoryPath);
             Directory.CreateDirectory(versionedDirectoryPath);
 
 
-            var tasks = new List<Task>();
-
-            double ffmpegPercent = 0;
-            double fontPercent = 0;
-            var syncLock = new object();
-
-            if (!File.Exists(info.ProbePath) || !File.Exists(info.Path))
+            if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
             {
             {
-                var ffmpegProgress = new ActionableProgress<double>();
-                ffmpegProgress.RegisterAction(p =>
-                {
-                    ffmpegPercent = p;
+                // ffmpeg not present. See if there's an older version we can start with
+                var existingVersion = GetExistingVersion(info, rootEncoderPath);
 
 
-                    lock (syncLock)
-                    {
-                        progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
-                    }
-                });
+                // No older version. Need to download and block until complete
+                if (existingVersion == null)
+                {
+                    await DownloadFFMpeg(versionedDirectoryPath, progress).ConfigureAwait(false);
+                }
+                else
+                {
+                    // Older version found. 
+                    // Start with that. Download new version in the background.
+                    var newPath = versionedDirectoryPath;
+                    Task.Run(() => DownloadFFMpegInBackground(newPath));
 
 
-                tasks.Add(DownloadFFMpeg(info, ffmpegProgress));
-            }
-            else
-            {
-                ffmpegPercent = 100;
-                progress.Report(50);
+                    info = existingVersion;
+                    versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
+                }
             }
             }
 
 
-            var fontProgress = new ActionableProgress<double>();
-            fontProgress.RegisterAction(p =>
+            await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false);
+
+            return info;
+        }
+
+        private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
+        {
+            var encoderFilename = Path.GetFileName(info.EncoderPath);
+            var probeFilename = Path.GetFileName(info.ProbePath);
+
+            foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly)
+                .ToList())
             {
             {
-                fontPercent = p;
+                var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList();
 
 
-                lock (syncLock)
+                var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
+                var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
+
+                if (!string.IsNullOrWhiteSpace(encoder) &&
+                    !string.IsNullOrWhiteSpace(probe))
                 {
                 {
-                    progress.Report((ffmpegPercent / 2) + (fontPercent / 2));
+                    return new FFMpegInfo
+                    {
+                         EncoderPath = encoder,
+                         ProbePath = probe,
+                         Version = Path.GetFileNameWithoutExtension(Path.GetDirectoryName(probe))
+                    };
                 }
                 }
-            });
-
-            tasks.Add(DownloadFonts(versionedDirectoryPath, fontProgress));
+            }
 
 
-            await Task.WhenAll(tasks).ConfigureAwait(false);
+            return null;
+        }
 
 
-            return info;
+        private async void DownloadFFMpegInBackground(string directory)
+        {
+            try
+            {
+                await DownloadFFMpeg(directory, new Progress<double>()).ConfigureAwait(false);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error downloading ffmpeg", ex);
+            }
         }
         }
 
 
-        private async Task DownloadFFMpeg(FFMpegInfo info, IProgress<double> progress)
+        private async Task DownloadFFMpeg(string directory, IProgress<double> progress)
         {
         {
             foreach (var url in FFMpegDownloadInfo.FfMpegUrls)
             foreach (var url in FFMpegDownloadInfo.FfMpegUrls)
             {
             {
@@ -114,7 +137,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
 
                     }).ConfigureAwait(false);
                     }).ConfigureAwait(false);
 
 
-                    ExtractFFMpeg(tempFile, Path.GetDirectoryName(info.Path));
+                    ExtractFFMpeg(tempFile, directory);
                     return;
                     return;
                 }
                 }
                 catch (HttpException ex)
                 catch (HttpException ex)
@@ -132,7 +155,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
 
         private void ExtractFFMpeg(string tempFile, string targetFolder)
         private void ExtractFFMpeg(string tempFile, string targetFolder)
         {
         {
-            _logger.Debug("Extracting ffmpeg from {0}", tempFile);
+            _logger.Info("Extracting ffmpeg from {0}", tempFile);
 
 
             var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString());
             var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString());
 
 
@@ -171,6 +194,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
 
         private void ExtractArchive(string archivePath, string targetPath)
         private void ExtractArchive(string archivePath, string targetPath)
         {
         {
+            _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+            
             if (string.Equals(FFMpegDownloadInfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
             if (string.Equals(FFMpegDownloadInfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
             {
             {
                 _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
                 _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
@@ -182,6 +207,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         }
         }
         private void Extract7zArchive(string archivePath, string targetPath)
         private void Extract7zArchive(string archivePath, string targetPath)
         {
         {
+            _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+
             _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
             _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
         }
         }
 
 
@@ -201,7 +228,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         /// Extracts the fonts.
         /// Extracts the fonts.
         /// </summary>
         /// </summary>
         /// <param name="targetPath">The target path.</param>
         /// <param name="targetPath">The target path.</param>
-        private async Task DownloadFonts(string targetPath, IProgress<double> progress)
+        /// <returns>Task.</returns>
+        private async Task DownloadFonts(string targetPath)
         {
         {
             try
             try
             {
             {
@@ -213,12 +241,19 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
 
                 var fontFile = Path.Combine(fontsDirectory, fontFilename);
                 var fontFile = Path.Combine(fontsDirectory, fontFilename);
 
 
-                if (!File.Exists(fontFile))
+                if (File.Exists(fontFile))
                 {
                 {
-                    await DownloadFontFile(fontsDirectory, fontFilename, progress).ConfigureAwait(false);
+                    await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
+                }
+                else
+                {
+                    // Kick this off, but no need to wait on it
+                    Task.Run(async () =>
+                    {
+                        await DownloadFontFile(fontsDirectory, fontFilename, new Progress<double>()).ConfigureAwait(false);
+                        await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
+                    });
                 }
                 }
-
-                await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
             }
             }
             catch (HttpException ex)
             catch (HttpException ex)
             {
             {
@@ -230,8 +265,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                 // Don't let the server crash because of this
                 // Don't let the server crash because of this
                 _logger.ErrorException("Error writing ffmpeg font files", ex);
                 _logger.ErrorException("Error writing ffmpeg font files", ex);
             }
             }
-
-            progress.Report(100);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -325,19 +358,5 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                 }
                 }
             }
             }
         }
         }
-
-        /// <summary>
-        /// Gets the media tools path.
-        /// </summary>
-        /// <param name="create">if set to <c>true</c> [create].</param>
-        /// <returns>System.String.</returns>
-        private string GetMediaToolsPath(bool create)
-        {
-            var path = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
-
-            Directory.CreateDirectory(path);
-
-            return path;
-        }
     }
     }
 }
 }

+ 1 - 1
MediaBrowser.ServerApplication/FFMpeg/FFMpegInfo.cs

@@ -9,7 +9,7 @@
         /// Gets or sets the path.
         /// Gets or sets the path.
         /// </summary>
         /// </summary>
         /// <value>The path.</value>
         /// <value>The path.</value>
-        public string Path { get; set; }
+        public string EncoderPath { get; set; }
         /// <summary>
         /// <summary>
         /// Gets or sets the probe path.
         /// Gets or sets the probe path.
         /// </summary>
         /// </summary>

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

@@ -217,6 +217,9 @@
     <Content Include="dashboard-ui\css\images\items\folders\channels.png">
     <Content Include="dashboard-ui\css\images\items\folders\channels.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>
+    <Content Include="dashboard-ui\css\images\items\folders\folder.png">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\css\images\items\folders\games.png">
     <Content Include="dashboard-ui\css\images\items\folders\games.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
     </Content>

+ 52 - 0
OpenSubtitlesHandler/OpenSubtitles.cs

@@ -20,6 +20,8 @@ using System;
 using System.Text;
 using System.Text;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
 using OpenSubtitlesHandler.Console;
 using OpenSubtitlesHandler.Console;
 using XmlRpcHandler;
 using XmlRpcHandler;
 
 
@@ -96,6 +98,56 @@ namespace OpenSubtitlesHandler
             }
             }
             return new MethodResponseError("Fail", "Log in failed !");
             return new MethodResponseError("Fail", "Log in failed !");
         }
         }
+
+        public static async Task<IMethodResponse> LogInAsync(string userName, string password, string language, CancellationToken cancellationToken)
+        {
+            // Method call ..
+            List<IXmlRpcValue> parms = new List<IXmlRpcValue>();
+            parms.Add(new XmlRpcValueBasic(userName));
+            parms.Add(new XmlRpcValueBasic(password));
+            parms.Add(new XmlRpcValueBasic(language));
+            parms.Add(new XmlRpcValueBasic(XML_PRC_USERAGENT));
+            XmlRpcMethodCall call = new XmlRpcMethodCall("LogIn", parms);
+            OSHConsole.WriteLine("Sending LogIn request to the server ...", DebugCode.Good);
+
+            //File.WriteAllText(".\\request.txt", Encoding.UTF8.GetString(XmlRpcGenerator.Generate(call)));
+            // Send the request to the server
+            var stream = await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken)
+                .ConfigureAwait(false);
+
+            string response = Utilities.GetStreamString(stream);
+
+            if (!response.Contains("ERROR:"))
+            {
+                // No error occur, get and decode the response. We expect Struct here.
+                XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response);
+                if (calls.Length > 0)
+                {
+                    if (calls[0].Parameters.Count > 0)
+                    {
+                        XmlRpcValueStruct mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0];
+                        MethodResponseLogIn re = new MethodResponseLogIn("Success", "Log in successful.");
+                        foreach (XmlRpcStructMember MEMBER in mainStruct.Members)
+                        {
+                            switch (MEMBER.Name)
+                            {
+                                case "token": re.Token = TOKEN = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+                                case "seconds": re.Seconds = (double)MEMBER.Data.Data; OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+                                case "status": re.Status = MEMBER.Data.Data.ToString(); OSHConsole.WriteLine(MEMBER.Name + "= " + MEMBER.Data.Data.ToString()); break;
+                            }
+                        }
+                        return re;
+                    }
+                }
+            }
+            else
+            {
+                OSHConsole.WriteLine(response, DebugCode.Error);
+                return new MethodResponseError("Fail", response);
+            }
+            return new MethodResponseError("Fail", "Log in failed !");
+        }
+        
         /// <summary>
         /// <summary>
         /// Log out from the server. Call this to terminate the session.
         /// Log out from the server. Call this to terminate the session.
         /// </summary>
         /// </summary>

+ 16 - 4
OpenSubtitlesHandler/Utilities.cs

@@ -24,6 +24,7 @@ using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
 using System.Net;
 using System.Net;
 using System.Security.Cryptography;
 using System.Security.Cryptography;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 
 
@@ -161,7 +162,7 @@ namespace OpenSubtitlesHandler
         /// <returns>Response of the server or stream of error message as string started with 'ERROR:' keyword.</returns>
         /// <returns>Response of the server or stream of error message as string started with 'ERROR:' keyword.</returns>
         public static Stream SendRequest(byte[] request, string userAgent)
         public static Stream SendRequest(byte[] request, string userAgent)
         {
         {
-            return SendRequestAsync(request, userAgent).Result;
+            return SendRequestAsync(request, userAgent, CancellationToken.None).Result;
 
 
             //HttpWebRequest req = (HttpWebRequest)WebRequest.Create(XML_RPC_SERVER);
             //HttpWebRequest req = (HttpWebRequest)WebRequest.Create(XML_RPC_SERVER);
             //req.ContentType = "text/xml";
             //req.ContentType = "text/xml";
@@ -190,16 +191,27 @@ namespace OpenSubtitlesHandler
             //}
             //}
         }
         }
 
 
-        public static async Task<Stream> SendRequestAsync(byte[] request, string userAgent)
+        public static async Task<Stream> SendRequestAsync(byte[] request, string userAgent, CancellationToken cancellationToken)
         {
         {
             var options = new HttpRequestOptions
             var options = new HttpRequestOptions
             {
             {
                 RequestContentBytes = request,
                 RequestContentBytes = request,
                 RequestContentType = "text/xml",
                 RequestContentType = "text/xml",
-                UserAgent = "xmlrpc-epi-php/0.2 (PHP)",
-                Url = XML_RPC_SERVER
+                UserAgent = userAgent,
+                Host = "api.opensubtitles.org:80",
+                Url = XML_RPC_SERVER,
+
+                // Response parsing will fail with this enabled
+                EnableHttpCompression = false,
+
+                CancellationToken = cancellationToken
             };
             };
 
 
+            if (string.IsNullOrEmpty(options.UserAgent))
+            {
+                options.UserAgent = "xmlrpc-epi-php/0.2 (PHP)";
+            }
+
             var result = await HttpClient.Post(options).ConfigureAwait(false);
             var result = await HttpClient.Post(options).ConfigureAwait(false);
 
 
             return result.Content;
             return result.Content;

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff