Explorar o código

add subtitle management page

Luke Pulverenti %!s(int64=11) %!d(string=hai) anos
pai
achega
c8e4889ac7

+ 1 - 23
MediaBrowser.Api/ItemLookupService.cs

@@ -7,7 +7,6 @@ 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,16 +31,6 @@ 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>>
@@ -121,24 +110,13 @@ namespace MediaBrowser.Api
         private readonly IServerApplicationPaths _appPaths;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
-        private readonly ISubtitleManager _subtitleManager;
 
-        public ItemLookupService(IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager, ISubtitleManager subtitleManager)
+        public ItemLookupService(IProviderManager providerManager, IServerApplicationPaths appPaths, IFileSystem fileSystem, ILibraryManager libraryManager)
         {
             _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 - 36
MediaBrowser.Api/Library/LibraryService.cs

@@ -1,5 +1,4 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -35,21 +34,6 @@ namespace MediaBrowser.Api.Library
         public string Id { get; set; }
     }
 
-    [Route("/Videos/{Id}/Subtitles/{Index}", "GET")]
-    [Api(Description = "Gets an external subtitle file")]
-    public class GetSubtitle
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
-        public string Id { get; set; }
-
-        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
-        public int Index { get; set; }
-    }
-
     /// <summary>
     /// Class GetCriticReviews
     /// </summary>
@@ -305,25 +289,6 @@ namespace MediaBrowser.Api.Library
             return ToStaticFileResult(item.Path);
         }
 
-        public object Get(GetSubtitle request)
-        {
-            var subtitleStream = _itemRepo.GetMediaStreams(new MediaStreamQuery
-            {
-
-                Index = request.Index,
-                ItemId = new Guid(request.Id),
-                Type = MediaStreamType.Subtitle
-
-            }).FirstOrDefault();
-
-            if (subtitleStream == null)
-            {
-                throw new ResourceNotFoundException();
-            }
-
-            return ToStaticFileResult(subtitleStream.Path);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 162 - 0
MediaBrowser.Api/Library/SubtitleService.cs

@@ -0,0 +1,162 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Subtitles;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using ServiceStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Library
+{
+    [Route("/Videos/{Id}/Subtitles/{Index}", "GET", Summary = "Gets an external subtitle file")]
+    public class GetSubtitle
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
+        public int Index { get; set; }
+    }
+
+    [Route("/Videos/{Id}/Subtitles/{Index}", "DELETE", Summary = "Deletes an external subtitle file")]
+    public class DeleteSubtitle
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "Index", Description = "The subtitle stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "DELETE")]
+        public int Index { 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/{Id}/RemoteSearch/Subtitles/Providers", "GET")]
+    public class GetSubtitleProviders : IReturn<List<SubtitleProviderInfo>>
+    {
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    [Route("/Items/{Id}/RemoteSearch/Subtitles/{SubtitleId}", "POST")]
+    public class DownloadRemoteSubtitles : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string Id { get; set; }
+
+        [ApiMember(Name = "SubtitleId", Description = "SubtitleId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
+        public string SubtitleId { get; set; }
+    }
+
+    [Route("/Providers/Subtitles/Subtitles/{Id}", "GET")]
+    public class GetRemoteSubtitles : IReturnVoid
+    {
+        [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
+        public string Id { get; set; }
+    }
+
+    public class SubtitleService : BaseApiService
+    {
+        private readonly ILibraryManager _libraryManager;
+        private readonly ISubtitleManager _subtitleManager;
+        private readonly IItemRepository _itemRepo;
+
+        public SubtitleService(ILibraryManager libraryManager, ISubtitleManager subtitleManager, IItemRepository itemRepo)
+        {
+            _libraryManager = libraryManager;
+            _subtitleManager = subtitleManager;
+            _itemRepo = itemRepo;
+        }
+
+        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(GetSubtitle request)
+        {
+            var subtitleStream = _itemRepo.GetMediaStreams(new MediaStreamQuery
+            {
+
+                Index = request.Index,
+                ItemId = new Guid(request.Id),
+                Type = MediaStreamType.Subtitle
+
+            }).FirstOrDefault();
+
+            if (subtitleStream == null)
+            {
+                throw new ResourceNotFoundException();
+            }
+
+            return ToStaticFileResult(subtitleStream.Path);
+        }
+
+        public void Delete(DeleteSubtitle request)
+        {
+            var task = _subtitleManager.DeleteSubtitles(request.Id, request.Index);
+
+            Task.WaitAll(task);
+        }
+
+        public object Get(GetSubtitleProviders request)
+        {
+            var result = _subtitleManager.GetProviders(request.Id);
+
+            return ToOptimizedResult(result);
+        }
+
+        public object Get(GetRemoteSubtitles request)
+        {
+            var result = _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).Result;
+
+            return ResultFactory.GetResult(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
+        }
+
+        public void Post(DownloadRemoteSubtitles request)
+        {
+            var video = (Video)_libraryManager.GetItemById(request.Id);
+
+            Task.Run(async () =>
+            {
+                try
+                {
+                    await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
+                        .ConfigureAwait(false);
+
+                    await video.RefreshMetadata(new MetadataRefreshOptions(), CancellationToken.None).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error downloading subtitles", ex);
+                }
+
+            });
+        }
+    }
+}

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

@@ -68,6 +68,7 @@
     <Compile Include="ChannelService.cs" />
     <Compile Include="Dlna\DlnaServerService.cs" />
     <Compile Include="Dlna\DlnaService.cs" />
+    <Compile Include="Library\SubtitleService.cs" />
     <Compile Include="Movies\CollectionService.cs" />
     <Compile Include="Music\AlbumsService.cs" />
     <Compile Include="AppThemeService.cs" />

+ 11 - 6
MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs

@@ -34,17 +34,22 @@ namespace MediaBrowser.Controller.Providers
         /// <summary>
         /// Providers will be executed based on default rules
         /// </summary>
-        EnsureMetadata,
+        EnsureMetadata = 0,
 
         /// <summary>
         /// No providers will be executed
         /// </summary>
-        None,
+        None = 1,
 
         /// <summary>
         /// All providers will be executed to search for new metadata
         /// </summary>
-        FullRefresh
+        FullRefresh = 2,
+
+        /// <summary>
+        /// The validation only
+        /// </summary>
+        ValidationOnly = 3
     }
 
     public enum ImageRefreshMode
@@ -52,16 +57,16 @@ namespace MediaBrowser.Controller.Providers
         /// <summary>
         /// The default
         /// </summary>
-        Default,
+        Default = 0,
 
         /// <summary>
         /// Existing images will be validated
         /// </summary>
-        ValidationOnly,
+        ValidationOnly = 1,
 
         /// <summary>
         /// All providers will be executed to search for new metadata
         /// </summary>
-        FullRefresh
+        FullRefresh = 2
     }
 }

+ 23 - 2
MediaBrowser.Controller/Subtitles/ISubtitleManager.cs

@@ -39,12 +39,33 @@ namespace MediaBrowser.Controller.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);
+
+        /// <summary>
+        /// Gets the remote subtitles.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{SubtitleResponse}.</returns>
+        Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Deletes the subtitles.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <param name="index">The index.</param>
+        /// <returns>Task.</returns>
+        Task DeleteSubtitles(string itemId, int index);
+
+        /// <summary>
+        /// Gets the providers.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>IEnumerable{SubtitleProviderInfo}.</returns>
+        IEnumerable<SubtitleProviderInfo> GetProviders(string itemId);
     }
 }

+ 1 - 0
MediaBrowser.Controller/Subtitles/SubtitleResponse.cs

@@ -6,6 +6,7 @@ namespace MediaBrowser.Controller.Subtitles
     {
         public string Language { get; set; }
         public string Format { get; set; }
+        public bool IsForced { get; set; }
         public Stream Stream { get; set; }
     }
 }

+ 3 - 0
MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs

@@ -21,8 +21,11 @@ namespace MediaBrowser.Controller.Subtitles
         public long? RuntimeTicks { get; set; }
         public Dictionary<string, string> ProviderIds { get; set; }
 
+        public bool SearchAllProviders { get; set; }
+
         public SubtitleSearchRequest()
         {
+            SearchAllProviders = true;
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
         }
     }

+ 1 - 2
MediaBrowser.Model/Entities/LibraryUpdateInfo.cs

@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
 
 namespace MediaBrowser.Model.Entities
 {

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

@@ -16,4 +16,10 @@ namespace MediaBrowser.Model.Providers
         public int? DownloadCount { get; set; }
         public bool? IsHashMatch { get; set; }
     }
+
+    public class SubtitleProviderInfo
+    {
+        public string Name { get; set; }
+        public string Id { get; set; }
+    }
 }

+ 1 - 1
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
             var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
 
-            return prober.ProbeVideo(item, directoryService, cancellationToken);
+            return prober.ProbeVideo(item, directoryService, true, cancellationToken);
         }
 
         public Task<ItemUpdateType> FetchAudioInfo<T>(T item, CancellationToken cancellationToken)

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

@@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _subtitleManager = subtitleManager;
         }
 
-        public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, bool enableSubtitleDownloading, CancellationToken cancellationToken)
             where T : Video
         {
             var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false);
@@ -105,7 +105,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, directoryService).ConfigureAwait(false);
+                await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, directoryService, enableSubtitleDownloading).ConfigureAwait(false);
 
             }
             finally
@@ -160,7 +160,7 @@ namespace MediaBrowser.Providers.MediaInfo
             return result;
         }
 
-        protected async Task Fetch(Video video, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount, BlurayDiscInfo blurayInfo, IDirectoryService directoryService)
+        protected async Task Fetch(Video video, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount, BlurayDiscInfo blurayInfo, IDirectoryService directoryService, bool enableSubtitleDownloading)
         {
             var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
             var mediaStreams = mediaInfo.MediaStreams;
@@ -208,7 +208,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
             }
 
-            await AddExternalSubtitles(video, mediaStreams, directoryService, cancellationToken).ConfigureAwait(false);
+            await AddExternalSubtitles(video, mediaStreams, directoryService, enableSubtitleDownloading, cancellationToken).ConfigureAwait(false);
 
             FetchWtvInfo(video, data);
 
@@ -416,13 +416,17 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// <param name="video">The video.</param>
         /// <param name="currentStreams">The current streams.</param>
-        private async Task AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService, CancellationToken cancellationToken)
+        /// <param name="directoryService">The directory service.</param>
+        /// <param name="enableSubtitleDownloading">if set to <c>true</c> [enable subtitle downloading].</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        private async Task AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService, bool enableSubtitleDownloading, CancellationToken cancellationToken)
         {
             var subtitleResolver = new SubtitleResolver(_localization);
 
             var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, currentStreams.Count, directoryService, false).ToList();
 
-            if ((_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
+            if (enableSubtitleDownloading && (_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
                 video is Episode) ||
                 (_config.Configuration.SubtitleOptions.DownloadMovieSubtitles &&
                 video is Movie))

+ 5 - 2
MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs

@@ -124,7 +124,10 @@ namespace MediaBrowser.Providers.MediaInfo
                 Name = video.Name,
                 ParentIndexNumber = video.ParentIndexNumber,
                 ProductionYear = video.ProductionYear,
-                ProviderIds = video.ProviderIds
+                ProviderIds = video.ProviderIds,
+
+                // Stop as soon as we find something
+                SearchAllProviders = false
             };
 
             var episode = video as Episode;
@@ -143,7 +146,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 if (result != null)
                 {
-                    await _subtitleManager.DownloadSubtitles(video, result.Id, result.ProviderName, cancellationToken)
+                    await _subtitleManager.DownloadSubtitles(video, result.Id, cancellationToken)
                             .ConfigureAwait(false);
 
                     return true;

+ 138 - 10
MediaBrowser.Providers/Subtitles/SubtitleManager.cs

@@ -1,8 +1,10 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Extensions;
+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.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Model.Entities;
@@ -23,12 +25,16 @@ namespace MediaBrowser.Providers.Subtitles
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryMonitor _monitor;
+        private readonly ILibraryManager _libraryManager;
+        private readonly IItemRepository _itemRepo;
 
-        public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor)
+        public SubtitleManager(ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor, ILibraryManager libraryManager, IItemRepository itemRepo)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _monitor = monitor;
+            _libraryManager = libraryManager;
+            _itemRepo = itemRepo;
         }
 
         public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders)
@@ -38,15 +44,45 @@ namespace MediaBrowser.Providers.Subtitles
 
         public async Task<IEnumerable<RemoteSubtitleInfo>> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken)
         {
+            var contentType = request.ContentType;
             var providers = _subtitleProviders
-                .Where(i => i.SupportedMediaTypes.Contains(request.ContentType))
+                .Where(i => i.SupportedMediaTypes.Contains(contentType))
                 .ToList();
 
+            // If not searching all, search one at a time until something is found
+            if (!request.SearchAllProviders)
+            {
+                foreach (var provider in providers)
+                {
+                    try
+                    {
+                        var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
+
+                        var list = searchResults.ToList();
+
+                        if (list.Count > 0)
+                        {
+                            Normalize(list);
+                            return list;
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error downloading subtitles from {0}", ex, provider.Name);
+                    }
+                }
+                return new List<RemoteSubtitleInfo>();
+            }
+
             var tasks = providers.Select(async i =>
             {
                 try
                 {
-                    return await i.Search(request, cancellationToken).ConfigureAwait(false);
+                    var searchResults = await i.Search(request, cancellationToken).ConfigureAwait(false);
+
+                    var list = searchResults.ToList();
+                    Normalize(list);
+                    return list;
                 }
                 catch (Exception ex)
                 {
@@ -62,17 +98,21 @@ namespace MediaBrowser.Providers.Subtitles
 
         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);
+            var response = await GetRemoteSubtitles(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());
+                var savePath = Path.Combine(Path.GetDirectoryName(video.Path),
+                    Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLower());
+
+                if (response.IsForced)
+                {
+                    savePath += ".forced";
+                }
+
+                savePath += "." + response.Format.ToLower();
 
                 _logger.Info("Saving subtitles to {0}", savePath);
 
@@ -139,5 +179,93 @@ namespace MediaBrowser.Providers.Subtitles
 
             return SearchSubtitles(request, cancellationToken);
         }
+
+        private void Normalize(IEnumerable<RemoteSubtitleInfo> subtitles)
+        {
+            foreach (var sub in subtitles)
+            {
+                sub.Id = GetProviderId(sub.ProviderName) + "_" + sub.Id;
+            }
+        }
+
+        private string GetProviderId(string name)
+        {
+            return name.ToLower().GetMD5().ToString("N");
+        }
+
+        private ISubtitleProvider GetProvider(string id)
+        {
+            return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name)));
+        }
+
+        public Task DeleteSubtitles(string itemId, int index)
+        {
+            var stream = _itemRepo.GetMediaStreams(new MediaStreamQuery
+            {
+                Index = index,
+                ItemId = new Guid(itemId),
+                Type = MediaStreamType.Subtitle
+
+            }).First();
+
+            var path = stream.Path;
+            _monitor.ReportFileSystemChangeBeginning(path);
+
+            try
+            {
+                File.Delete(path);
+            }
+            finally
+            {
+                _monitor.ReportFileSystemChangeComplete(path, false);
+            }
+
+            return _libraryManager.GetItemById(itemId).RefreshMetadata(new MetadataRefreshOptions
+            {
+                ImageRefreshMode = ImageRefreshMode.ValidationOnly,
+                MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
+
+            }, CancellationToken.None);
+        }
+
+        public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken)
+        {
+            var parts = id.Split(new[] { '_' }, 2);
+
+            var provider = GetProvider(parts.First());
+            id = parts.Last();
+
+            return provider.GetSubtitles(id, cancellationToken);
+        }
+
+        public IEnumerable<SubtitleProviderInfo> GetProviders(string itemId)
+        {
+            var video = _libraryManager.GetItemById(itemId) as Video;
+            VideoContentType mediaType;
+
+            if (video is Episode)
+            {
+                mediaType = VideoContentType.Episode;
+            }
+            else if (video is Movie)
+            {
+                mediaType = VideoContentType.Movie;
+            }
+            else
+            {
+                // These are the only supported types
+                return new List<SubtitleProviderInfo>();
+            }
+
+            var providers = _subtitleProviders
+                .Where(i => i.SupportedMediaTypes.Contains(mediaType))
+                .ToList();
+
+            return providers.Select(i => new SubtitleProviderInfo
+            {
+                Name = i.Name,
+                Id = GetProviderId(i.Name)
+            });
+        }
     }
 }

+ 1 - 1
MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -44,7 +44,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
         /// <summary>
         /// The library update duration
         /// </summary>
-        private const int LibraryUpdateDuration = 20000;
+        private const int LibraryUpdateDuration = 5000;
 
         public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger)
         {

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

@@ -759,5 +759,7 @@
 	"LabelEpisodeNumber": "Episode number",
 	"LabelEndingEpisodeNumber": "Ending episode number",
 	"HeaderTypeText": "Enter Text",
-	"LabelTypeText": "Text"
+	"LabelTypeText": "Text",
+	"HeaderSearchForSubtitles": "Search for Subtitles",
+	"MessageNoSubtitleSearchResultsFound": "No search results founds."
 }

+ 1 - 1
MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -138,7 +138,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
                 if (controller == null)
                 {
-                    controller = new WebSocketController(session, _appHost);
+                    controller = new WebSocketController(session, _appHost, _logger);
                 }
 
                 controller.Sockets.Add(message.Connection);

+ 49 - 37
MediaBrowser.Server.Implementations/Session/WebSocketController.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.System;
@@ -19,11 +20,13 @@ namespace MediaBrowser.Server.Implementations.Session
         public List<IWebSocketConnection> Sockets { get; private set; }
 
         private readonly IServerApplicationHost _appHost;
+        private readonly ILogger _logger;
 
-        public WebSocketController(SessionInfo session, IServerApplicationHost appHost)
+        public WebSocketController(SessionInfo session, IServerApplicationHost appHost, ILogger logger)
         {
             Session = session;
             _appHost = appHost;
+            _logger = logger;
             Sockets = new List<IWebSocketConnection>();
         }
 
@@ -35,11 +38,17 @@ namespace MediaBrowser.Server.Implementations.Session
             }
         }
 
-        private IWebSocketConnection GetActiveSocket()
+        private IEnumerable<IWebSocketConnection> GetActiveSockets()
         {
-            var socket = Sockets
+            return Sockets
                 .OrderByDescending(i => i.LastActivityDate)
-                .FirstOrDefault(i => i.State == WebSocketState.Open);
+                .Where(i => i.State == WebSocketState.Open);
+        }
+
+        private IWebSocketConnection GetActiveSocket()
+        {
+            var socket = GetActiveSockets()
+                .FirstOrDefault();
 
             if (socket == null)
             {
@@ -51,9 +60,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<PlayRequest>
+            return SendMessage(new WebSocketMessage<PlayRequest>
             {
                 MessageType = "Play",
                 Data = command
@@ -63,9 +70,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<PlaystateRequest>
+            return SendMessage(new WebSocketMessage<PlaystateRequest>
             {
                 MessageType = "Playstate",
                 Data = command
@@ -75,9 +80,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-            
-            return socket.SendAsync(new WebSocketMessage<LibraryUpdateInfo>
+            return SendMessages(new WebSocketMessage<LibraryUpdateInfo>
             {
                 MessageType = "LibraryChanged",
                 Data = info
@@ -92,9 +95,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <returns>Task.</returns>
         public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<SystemInfo>
+            return SendMessages(new WebSocketMessage<SystemInfo>
             {
                 MessageType = "RestartRequired",
                 Data = _appHost.GetSystemInfo()
@@ -111,9 +112,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <returns>Task.</returns>
         public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<UserDataChangeInfo>
+            return SendMessages(new WebSocketMessage<UserDataChangeInfo>
             {
                 MessageType = "UserDataChanged",
                 Data = info
@@ -128,9 +127,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <returns>Task.</returns>
         public Task SendServerShutdownNotification(CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<string>
+            return SendMessages(new WebSocketMessage<string>
             {
                 MessageType = "ServerShuttingDown",
                 Data = string.Empty
@@ -145,9 +142,7 @@ namespace MediaBrowser.Server.Implementations.Session
         /// <returns>Task.</returns>
         public Task SendServerRestartNotification(CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<string>
+            return SendMessages(new WebSocketMessage<string>
             {
                 MessageType = "ServerRestarting",
                 Data = string.Empty
@@ -157,9 +152,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<GeneralCommand>
+            return SendMessage(new WebSocketMessage<GeneralCommand>
             {
                 MessageType = "GeneralCommand",
                 Data = command
@@ -169,9 +162,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            return SendMessages(new WebSocketMessage<SessionInfoDto>
             {
                 MessageType = "SessionEnded",
                 Data = sessionInfo
@@ -181,9 +172,7 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            return SendMessages(new WebSocketMessage<SessionInfoDto>
             {
                 MessageType = "PlaybackStart",
                 Data = sessionInfo
@@ -193,14 +182,37 @@ namespace MediaBrowser.Server.Implementations.Session
 
         public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
         {
-            var socket = GetActiveSocket();
-
-            return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
+            return SendMessages(new WebSocketMessage<SessionInfoDto>
             {
                 MessageType = "PlaybackStopped",
                 Data = sessionInfo
 
             }, cancellationToken);
         }
+
+        private Task SendMessage<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
+        {
+            var socket = GetActiveSocket();
+
+            return socket.SendAsync(message, cancellationToken);
+        }
+
+        private Task SendMessages<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
+        {
+            var tasks = GetActiveSockets().Select(i => Task.Run(async () =>
+            {
+                try
+                {
+                    await i.SendAsync(message, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error sending web socket message", ex);
+                }
+
+            }, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
     }
 }

+ 1 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -537,7 +537,7 @@ namespace MediaBrowser.ServerApplication
 
             RegisterSingleInstance<IEncryptionManager>(new EncryptionManager());
 
-            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor);
+            SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, ItemRepository);
             RegisterSingleInstance(SubtitleManager);
 
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));

+ 1 - 0
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -550,6 +550,7 @@ namespace MediaBrowser.WebDashboard.Api
                                 "editcollectionitems.js",
                                 "edititemmetadata.js",
                                 "edititemimages.js",
+                                "edititemsubtitles.js",
                                 "encodingsettings.js",
                                 "gamesrecommendedpage.js",
                                 "gamesystemspage.js",

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

@@ -307,6 +307,9 @@
     <Content Include="dashboard-ui\editcollectionitems.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\edititemsubtitles.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\encodingsettings.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -616,6 +619,9 @@
     <Content Include="dashboard-ui\scripts\editcollectionitems.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\edititemsubtitles.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\encodingsettings.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>