소스 검색

beginning remote subtitle downloading

Luke Pulverenti 11 년 전
부모
커밋
0d025f7fb6
49개의 변경된 파일1030개의 추가작업 그리고 296개의 파일을 삭제
  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")]
         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; }
 
         [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.IO;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using ServiceStack;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 
@@ -70,6 +72,32 @@ namespace MediaBrowser.Api.Images
         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>
     /// Class ImageByNameService
     /// </summary>
@@ -89,6 +117,60 @@ namespace MediaBrowser.Api.Images
             _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>
         /// Gets the specified request.
         /// </summary>
@@ -118,7 +200,8 @@ namespace MediaBrowser.Api.Images
 
             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);
 
                 if (!string.IsNullOrEmpty(path))
@@ -134,7 +217,8 @@ namespace MediaBrowser.Api.Images
                 // Avoid implicitly captured closure
                 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);
 
                 if (!string.IsNullOrEmpty(path))
@@ -175,7 +259,7 @@ namespace MediaBrowser.Api.Images
 
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, currentRequest.Name + i))
                     .FirstOrDefault(File.Exists);
-                
+
                 if (!string.IsNullOrEmpty(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.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
@@ -14,6 +13,7 @@ using ServiceStack.Text.Controller;
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Threading;

+ 23 - 4
MediaBrowser.Api/ItemLookupService.cs

@@ -1,13 +1,13 @@
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Providers;
 using ServiceStack;
@@ -32,6 +32,16 @@ namespace MediaBrowser.Api
         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")]
     [Api(Description = "Gets external id infos for an item")]
     public class GetMovieRemoteSearchResults : RemoteSearchQuery<MovieInfo>, IReturn<List<RemoteSearchResult>>
@@ -107,19 +117,28 @@ namespace MediaBrowser.Api
 
     public class ItemLookupService : BaseApiService
     {
-        private readonly IDtoService _dtoService;
         private readonly IProviderManager _providerManager;
         private readonly IServerApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         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;
             _appPaths = appPaths;
             _fileSystem = fileSystem;
             _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)

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

@@ -69,7 +69,7 @@ namespace MediaBrowser.Api.UserLibrary
         /// Filters to apply to the results
         /// </summary>
         /// <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; }
 
         /// <summary>

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

@@ -521,6 +521,9 @@ namespace MediaBrowser.Api.UserLibrary
 
                 case ItemFilter.IsNotFolder:
                     return items.Where(item => !item.IsFolder);
+
+                case ItemFilter.IsRecentlyAdded:
+                    return items.Where(item => (DateTime.UtcNow - item.DateCreated).TotalDays <= 10);
             }
 
             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.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.KeepAlive = true;
@@ -124,6 +122,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
             request.Pipelined = true;
             request.Timeout = 20000;
 
+            if (!string.IsNullOrEmpty(options.Host))
+            {
+                request.Host = options.Host;
+            }
+
 #if !__MonoCS__
             // This is a hack to prevent KeepAlive from getting disabled internally by the HttpWebRequest
             // May need to remove this for mono
@@ -234,9 +237,11 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager
                 !string.IsNullOrEmpty(options.RequestContent) ||
                 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.ContentLength = 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 IFileSystem _fileSystem;
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
         /// </summary>
@@ -74,9 +74,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
 
             progress.Report(90);
 
+            minDateModified = DateTime.UtcNow.AddDays(-3);
+
             try
             {
-                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, DateTime.MaxValue, progress);
+                DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
             }
             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>
         /// Gets or sets the progress.
         /// </summary>
@@ -76,8 +82,6 @@ namespace MediaBrowser.Common.Net
         public bool LogRequest { get; set; }
 
         public bool LogErrorResponseBody { get; set; }
-        
-        public HttpRequestCachePolicy CachePolicy { get; set; }
 
         private string GetHeaderValue(string name)
         {
@@ -96,17 +100,9 @@ namespace MediaBrowser.Common.Net
             EnableHttpCompression = true;
             BufferContent = true;
 
-            CachePolicy = HttpRequestCachePolicy.None;
-
             RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
             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\IMetadataService.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\MetadataRefreshOptions.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.Providers;
 using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace MediaBrowser.Controller.Providers
+namespace MediaBrowser.Controller.Subtitles
 {
     public interface ISubtitleProvider
     {
@@ -22,12 +23,20 @@ namespace MediaBrowser.Controller.Providers
         IEnumerable<SubtitleMediaType> SupportedMediaTypes { get; }
 
         /// <summary>
-        /// Gets the subtitles.
+        /// 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>
+        /// Gets the subtitles.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task{SubtitleResponse}.</returns>
-        Task<SubtitleResponse> GetSubtitles(SubtitleRequest request, CancellationToken cancellationToken);
+        Task<SubtitleResponse> GetSubtitles(string id, CancellationToken cancellationToken);
     }
 
     public enum SubtitleMediaType
@@ -38,12 +47,12 @@ namespace MediaBrowser.Controller.Providers
 
     public class SubtitleResponse
     {
+        public string Language { get; set; }
         public string Format { get; set; }
-        public bool HasContent { get; set; }
         public Stream Stream { get; set; }
     }
 
-    public class SubtitleRequest : IHasProviderIds
+    public class SubtitleSearchRequest : IHasProviderIds
     {
         public string Language { get; set; }
 
@@ -58,7 +67,7 @@ namespace MediaBrowser.Controller.Providers
         public int? ProductionYear { get; set; }
         public Dictionary<string, string> ProviderIds { get; set; }
 
-        public SubtitleRequest()
+        public SubtitleSearchRequest()
         {
             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 IServerConfigurationManager _config;
 
+        public DateTime DateLastActivity { get; private set; }
+
         public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
         {
             Properties = deviceProperties;
@@ -386,6 +388,8 @@ namespace MediaBrowser.Dlna.PlayTo
             {
                 var transportState = await GetTransportInfo().ConfigureAwait(false);
 
+                DateLastActivity = DateTime.UtcNow;
+
                 if (transportState.HasValue)
                 {
                     // 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)
         {
             _session = session;
@@ -75,6 +77,24 @@ namespace MediaBrowser.Dlna.PlayTo
             _device.Start();
 
             _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()
@@ -571,10 +591,21 @@ namespace MediaBrowser.Dlna.PlayTo
                 _device.PlaybackStopped -= _device_PlaybackStopped;
                 _ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived;
 
+                DisposeUpdateTimer();
+
                 _device.Dispose();
             }
         }
 
+        private void DisposeUpdateTimer()
+        {
+            if (_updateTimer != null)
+            {
+                _updateTimer.Dispose();
+                _updateTimer = null;
+            }
+        }
+
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
         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\SrtParser.cs" />
     <Compile Include="Subtitles\SsaParser.cs" />
-    <Compile Include="Subtitles\SubtitleInfo.cs" />
+    <Compile Include="Subtitles\SubtitleTrackInfo.cs" />
   </ItemGroup>
   <ItemGroup>
     <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
     {
-        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
 {
-    public class SrtParser
+    public class SrtParser : ISubtitleParser
     {
-        public SubtitleInfo Parse(Stream stream)
+        public SubtitleTrackInfo Parse(Stream stream)
         {
             throw new NotImplementedException();
         }

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

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

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

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

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

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

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

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

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

@@ -760,7 +760,7 @@ namespace MediaBrowser.Model.ApiClient
         /// </summary>
         /// <param name="options">The options.</param>
         /// <returns>System.String.</returns>
-        string GetSubtitleUrl(SubtitleOptions options);
+        string GetSubtitleUrl(SubtitleDownloadOptions options);
         
         /// <summary>
         /// 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 SubtitleOptions SubtitleOptions { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -284,6 +286,8 @@ namespace MediaBrowser.Model.Configuration
             UICulture = "en-us";
 
             NotificationOptions = new NotificationOptions();
+
+            SubtitleOptions = new SubtitleOptions();
         }
     }
 
@@ -311,4 +315,17 @@ namespace MediaBrowser.Model.Configuration
         public string From { 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 class SubtitleOptions
+    public class SubtitleDownloadOptions
     {
         /// <summary>
         /// Gets or sets the item identifier.

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

@@ -139,6 +139,7 @@
     <Compile Include="Notifications\NotificationsSummary.cs" />
     <Compile Include="Providers\RemoteImageResult.cs" />
     <Compile Include="Providers\RemoteSearchResult.cs" />
+    <Compile Include="Providers\RemoteSubtitleInfo.cs" />
     <Compile Include="Querying\ArtistsQuery.cs" />
     <Compile Include="Querying\EpisodeQuery.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>
         IsFavorite = 5,
         /// <summary>
+        /// The is recently added
+        /// </summary>
+        IsRecentlyAdded = 6,
+        /// <summary>
         /// The item is resumable
         /// </summary>
         IsResumable = 7,

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

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

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

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
@@ -10,15 +11,16 @@ using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Serialization;
 using System;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using System.Linq;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
@@ -45,6 +47,8 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IJsonSerializer _json;
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _config;
+        private readonly ISubtitleManager _subtitleManager;
 
         public string Name
         {
@@ -96,7 +100,7 @@ namespace MediaBrowser.Providers.MediaInfo
             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;
             _isoManager = isoManager;
@@ -108,6 +112,8 @@ namespace MediaBrowser.Providers.MediaInfo
             _json = json;
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
+            _config = config;
+            _subtitleManager = subtitleManager;
         }
 
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
@@ -134,7 +140,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 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);
         }
@@ -165,7 +171,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 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);
                 }

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

@@ -2,12 +2,16 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Localization;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Logging;
@@ -35,10 +39,12 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IJsonSerializer _json;
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
+        private readonly IServerConfigurationManager _config;
+        private readonly ISubtitleManager _subtitleManager;
 
         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;
             _isoManager = isoManager;
@@ -50,6 +56,8 @@ namespace MediaBrowser.Providers.MediaInfo
             _json = json;
             _encodingManager = encodingManager;
             _fileSystem = fileSystem;
+            _config = config;
+            _subtitleManager = subtitleManager;
         }
 
         public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
@@ -118,7 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
             cancellationToken.ThrowIfCancellationRequested();
 
             var idString = item.Id.ToString("N");
-            var cachePath = Path.Combine(_appPaths.CachePath, 
+            var cachePath = Path.Combine(_appPaths.CachePath,
                 "ffprobe-video",
                 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);
             }
 
-            AddExternalSubtitles(video, mediaStreams, directoryService);
+            await AddExternalSubtitles(video, mediaStreams, directoryService, cancellationToken).ConfigureAwait(false);
 
             FetchWtvInfo(video, data);
 
@@ -247,7 +255,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
             }
 
-            info.StartPositionTicks = chapter.start/100;
+            info.StartPositionTicks = chapter.start / 100;
 
             return info;
         }
@@ -450,11 +458,42 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// <param name="video">The video.</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 startIndex = currentStreams.Count;
             var streams = new List<MediaStream>();
 
             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>
@@ -627,7 +664,7 @@ namespace MediaBrowser.Providers.MediaInfo
         {
             var path = mount == null ? item.Path : mount.MountedPath;
             var dvd = new Dvd(path);
-            
+
             var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
 
             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.Subtitles;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Providers;
 using OpenSubtitlesHandler;
 using System;
 using System.Collections.Generic;
@@ -20,9 +22,9 @@ namespace MediaBrowser.Providers.Subtitles
         private readonly IHttpClient _httpClient;
         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;
         }
 
@@ -36,39 +38,71 @@ namespace MediaBrowser.Providers.Subtitles
             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)
         {
-            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);
             long imdbId;
 
             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)
             {
                 case SubtitleMediaType.Episode:
                     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;
                 case SubtitleMediaType.Movie:
                     if (string.IsNullOrEmpty(request.Name))
                     {
-                        _logger.Debug("Information Missing");
-                        return response;
+                        _logger.Debug("Movie name missing");
+                        return new List<RemoteSubtitleInfo>();
                     }
                     break;
             }
@@ -76,16 +110,18 @@ namespace MediaBrowser.Providers.Subtitles
             if (string.IsNullOrEmpty(request.MediaPath))
             {
                 _logger.Debug("Path Missing");
-                return response;
+                return new List<RemoteSubtitleInfo>();
             }
 
             Utilities.HttpClient = _httpClient;
             OpenSubtitles.SetUserAgent("OS Test User Agent");
-            var loginResponse = OpenSubtitles.LogIn("", "", "en");
+
+            var loginResponse = await OpenSubtitles.LogInAsync("", "", "en", cancellationToken).ConfigureAwait(false);
+
             if (!(loginResponse is MethodResponseLogIn))
             {
                 _logger.Debug("Login error");
-                return response;
+                return new List<RemoteSubtitleInfo>();
             }
 
             var subLanguageId = request.Language;
@@ -105,54 +141,42 @@ namespace MediaBrowser.Providers.Subtitles
             var result = OpenSubtitles.SearchSubtitles(parms.ToArray());
             if (!(result is MethodResponseSubtitleSearch))
             {
-                _logger.Debug("invalid response type");
-                return null;
+                _logger.Debug("Invalid response type");
+                return new List<RemoteSubtitleInfo>();
             }
 
             Predicate<SubtitleSearchResult> mediaFilter =
                 x =>
                     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 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");
 
-            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)

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

@@ -93,7 +93,13 @@ namespace MediaBrowser.Server.Implementations.Collections
                 // Find an actual physical folder
                 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>
         public void Warn(object message)
         {
-            _logger.Warn(GetMesssage(message));
+            // Hide StringMapTypeDeserializer messages
+            // _logger.Warn(GetMesssage(message));
         }
 
         /// <summary>
@@ -216,7 +217,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
         /// <param name="args">The args.</param>
         public void WarnFormat(string format, params object[] args)
         {
-            _logger.Warn(format, args);
+            // Hide StringMapTypeDeserializer messages
+            // _logger.Warn(format, args);
         }
 
         /// <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.Globalization;
 using MediaBrowser.Model.Serialization;
-using MoreLinq;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -106,16 +105,13 @@ namespace MediaBrowser.Server.Implementations.Localization
         /// <returns>IEnumerable{CultureDto}.</returns>
         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>
@@ -124,28 +120,13 @@ namespace MediaBrowser.Server.Implementations.Localization
         /// <returns>IEnumerable{CountryInfo}.</returns>
         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>

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

@@ -627,5 +627,84 @@
 	"OptionSpecialFeatures": "Special Features",
 	"HeaderCollections": "Collections",
 	"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."
 }

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
MediaBrowser.Server.Implementations/Localization/countries.json


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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\MusicGenresPostScanTask.cs" />
     <Compile Include="Library\Validators\MusicGenresValidator.cs" />
-    <Compile Include="Library\Validators\PeoplePostScanTask.cs" />
     <Compile Include="Library\Validators\PeopleValidator.cs" />
     <Compile Include="Library\Validators\StudiosPostScanTask.cs" />
     <Compile Include="Library\Validators\StudiosValidator.cs" />
@@ -328,6 +327,8 @@
     <EmbeddedResource Include="Localization\Server\ms.json" />
     <EmbeddedResource Include="Localization\JavaScript\kk.json" />
     <EmbeddedResource Include="Localization\Server\kk.json" />
+    <EmbeddedResource Include="Localization\countries.json" />
+    <EmbeddedResource Include="Localization\cultures.json" />
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>

+ 9 - 2
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -31,11 +31,11 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.Themes;
 using MediaBrowser.Dlna;
 using MediaBrowser.Dlna.Eventing;
 using MediaBrowser.Dlna.Main;
-using MediaBrowser.Dlna.PlayTo;
 using MediaBrowser.Dlna.Server;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Encoder;
@@ -44,6 +44,7 @@ using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Updates;
 using MediaBrowser.Providers.Manager;
+using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Server.Implementations;
 using MediaBrowser.Server.Implementations.Channels;
 using MediaBrowser.Server.Implementations.Collections;
@@ -193,6 +194,7 @@ namespace MediaBrowser.ServerApplication
         private IProviderRepository ProviderRepository { get; set; }
 
         private INotificationManager NotificationManager { get; set; }
+        private ISubtitleManager SubtitleManager { get; set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -531,6 +533,9 @@ namespace MediaBrowser.ServerApplication
             NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager);
             RegisterSingleInstance(NotificationManager);
 
+            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor);
+            RegisterSingleInstance(SubtitleManager);
+
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
             var itemsTask = Task.Run(async () => await ConfigureItemRepositories().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);
 
-            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);
         }
 
@@ -710,6 +715,8 @@ namespace MediaBrowser.ServerApplication
 
             LiveTvManager.AddParts(GetExports<ILiveTvService>());
 
+            SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
+            
             SessionManager.AddParts(GetExports<ISessionControllerFactory>());
 
             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.IO;
 #endif
+using System.IO;
+using System.Text.RegularExpressions;
 
 namespace MediaBrowser.ServerApplication.FFMpeg
 {
@@ -32,7 +34,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     switch (arg)
                     {
                         case "Version":
-                            return "20140304";
+                            return "20140506";
                         case "FFMpegFilename":
                             return "ffmpeg.exe";
                         case "FFProbeFilename":
@@ -42,7 +44,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     }
                     break;
 
-                #if __MonoCS__
                 case PlatformID.Unix:
                     if (PlatformDetection.IsMac)
                     {
@@ -69,7 +70,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                             switch (arg)
                             {
                                 case "Version":
-                                    return "20140304";
+                                    return "20140506";
                                 case "FFMpegFilename":
                                     return "ffmpeg";
                                 case "FFProbeFilename":
@@ -85,7 +86,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                             switch (arg)
                             {
                                 case "Version":
-                                    return "20140304";
+                                    return "20140505";
                                 case "FFMpegFilename":
                                     return "ffmpeg";
                                 case "FFProbeFilename":
@@ -98,7 +99,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                     }
                     // Unsupported Unix platform
                     return "";
-#endif
             }
             return "";
         }
@@ -106,18 +106,17 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         private static string[] GetDownloadUrls()
         {
             var pid = Environment.OSVersion.Platform;
-            
+
             switch (pid)
             {
                 case PlatformID.Win32NT:
                     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)
                     {
                         return new[]
@@ -132,8 +131,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                         {
                             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[]
                             {
-                                "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 
-                    return new string[] {};
-#endif
+                    return new string[] { };
             }
-            return new string[] {};
+            return new string[] { };
         }
     }
 
-    #if __MonoCS__
     public static class PlatformDetection
     {
         public readonly static bool IsWindows;
@@ -166,34 +163,52 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         public readonly static bool IsX86_64;
         public readonly static bool IsArm;
 
-        static PlatformDetection ()
+        static PlatformDetection()
         {
             IsWindows = Path.DirectorySeparatorChar == '\\';
 
             //Don't call uname on windows
             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
             {
-                if (System.Environment.Is64BitOperatingSystem)
+                if (Environment.Is64BitOperatingSystem)
                     IsX86_64 = true;
                 else
                     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)
         {
-            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
             {
                 ProbePath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFProbeFilename),
-                Path = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
+                EncoderPath = Path.Combine(versionedDirectoryPath, FFMpegDownloadInfo.FFMpegFilename),
                 Version = FFMpegDownloadInfo.Version
             };
 
             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)
             {
@@ -114,7 +137,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
                     }).ConfigureAwait(false);
 
-                    ExtractFFMpeg(tempFile, Path.GetDirectoryName(info.Path));
+                    ExtractFFMpeg(tempFile, directory);
                     return;
                 }
                 catch (HttpException ex)
@@ -132,7 +155,7 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
         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());
 
@@ -171,6 +194,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
         private void ExtractArchive(string archivePath, string targetPath)
         {
+            _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+            
             if (string.Equals(FFMpegDownloadInfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
             {
                 _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
@@ -182,6 +207,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         }
         private void Extract7zArchive(string archivePath, string targetPath)
         {
+            _logger.Info("Extracting {0} to {1}", archivePath, targetPath);
+
             _zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
         }
 
@@ -201,7 +228,8 @@ namespace MediaBrowser.ServerApplication.FFMpeg
         /// Extracts the fonts.
         /// </summary>
         /// <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
             {
@@ -213,12 +241,19 @@ namespace MediaBrowser.ServerApplication.FFMpeg
 
                 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)
             {
@@ -230,8 +265,6 @@ namespace MediaBrowser.ServerApplication.FFMpeg
                 // Don't let the server crash because of this
                 _logger.ErrorException("Error writing ffmpeg font files", ex);
             }
-
-            progress.Report(100);
         }
 
         /// <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.
         /// </summary>
         /// <value>The path.</value>
-        public string Path { get; set; }
+        public string EncoderPath { get; set; }
         /// <summary>
         /// Gets or sets the probe path.
         /// </summary>

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

@@ -217,6 +217,9 @@
     <Content Include="dashboard-ui\css\images\items\folders\channels.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </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">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 52 - 0
OpenSubtitlesHandler/OpenSubtitles.cs

@@ -20,6 +20,8 @@ using System;
 using System.Text;
 using System.Collections.Generic;
 using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
 using OpenSubtitlesHandler.Console;
 using XmlRpcHandler;
 
@@ -96,6 +98,56 @@ namespace OpenSubtitlesHandler
             }
             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>
         /// Log out from the server. Call this to terminate the session.
         /// </summary>

+ 16 - 4
OpenSubtitlesHandler/Utilities.cs

@@ -24,6 +24,7 @@ using System.IO;
 using System.IO.Compression;
 using System.Net;
 using System.Security.Cryptography;
+using System.Threading;
 using System.Threading.Tasks;
 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>
         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);
             //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
             {
                 RequestContentBytes = request,
                 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);
 
             return result.Content;

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.