From c8e4889ac72b4b6fa01ffd0ccf293363ca5ce744 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 17 May 2014 00:24:10 -0400 Subject: [PATCH] add subtitle management page --- MediaBrowser.Api/ItemLookupService.cs | 24 +-- MediaBrowser.Api/Library/LibraryService.cs | 37 +--- MediaBrowser.Api/Library/SubtitleService.cs | 162 ++++++++++++++++++ MediaBrowser.Api/MediaBrowser.Api.csproj | 1 + .../Providers/MetadataRefreshOptions.cs | 17 +- .../Subtitles/ISubtitleManager.cs | 25 ++- .../Subtitles/SubtitleResponse.cs | 1 + .../Subtitles/SubtitleSearchRequest.cs | 3 + .../Entities/LibraryUpdateInfo.cs | 3 +- .../Providers/RemoteSubtitleInfo.cs | 6 + .../MediaInfo/FFProbeProvider.cs | 2 +- .../MediaInfo/FFProbeVideoInfo.cs | 16 +- .../MediaInfo/SubtitleDownloader.cs | 7 +- .../Subtitles/SubtitleManager.cs | 148 ++++++++++++++-- .../EntryPoints/LibraryChangedNotifier.cs | 2 +- .../Localization/Server/server.json | 4 +- .../Session/SessionWebSocketListener.cs | 2 +- .../Session/WebSocketController.cs | 86 ++++++---- .../ApplicationHost.cs | 2 +- .../Api/DashboardService.cs | 1 + .../MediaBrowser.WebDashboard.csproj | 6 + 21 files changed, 426 insertions(+), 129 deletions(-) create mode 100644 MediaBrowser.Api/Library/SubtitleService.cs diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index 86fdd6da87..ff11ad47c4 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/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> - { - [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, IReturn> @@ -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) diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 0e5a3ab25e..802df5cca6 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/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 - { - /// - /// Gets or sets the id. - /// - /// The id. - [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; } - } - /// /// Class GetCriticReviews /// @@ -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); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/Library/SubtitleService.cs b/MediaBrowser.Api/Library/SubtitleService.cs new file mode 100644 index 0000000000..78ae627eab --- /dev/null +++ b/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 + { + /// + /// Gets or sets the id. + /// + /// The id. + [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 + { + /// + /// Gets or sets the id. + /// + /// The id. + [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> + { + [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> + { + [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); + } + + }); + } + } +} diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index edbae3903e..62d5a6ce22 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -68,6 +68,7 @@ + diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index 780aa6a568..35e86fb87c 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -34,17 +34,22 @@ namespace MediaBrowser.Controller.Providers /// /// Providers will be executed based on default rules /// - EnsureMetadata, + EnsureMetadata = 0, /// /// No providers will be executed /// - None, + None = 1, /// /// All providers will be executed to search for new metadata /// - FullRefresh + FullRefresh = 2, + + /// + /// The validation only + /// + ValidationOnly = 3 } public enum ImageRefreshMode @@ -52,16 +57,16 @@ namespace MediaBrowser.Controller.Providers /// /// The default /// - Default, + Default = 0, /// /// Existing images will be validated /// - ValidationOnly, + ValidationOnly = 1, /// /// All providers will be executed to search for new metadata /// - FullRefresh + FullRefresh = 2 } } diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 8b0ef223c3..1d66d1505b 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -39,12 +39,33 @@ namespace MediaBrowser.Controller.Subtitles /// /// The video. /// The subtitle identifier. - /// Name of the provider. /// The cancellation token. /// Task. Task DownloadSubtitles(Video video, string subtitleId, - string providerName, CancellationToken cancellationToken); + + /// + /// Gets the remote subtitles. + /// + /// The identifier. + /// The cancellation token. + /// Task{SubtitleResponse}. + Task GetRemoteSubtitles(string id, CancellationToken cancellationToken); + + /// + /// Deletes the subtitles. + /// + /// The item identifier. + /// The index. + /// Task. + Task DeleteSubtitles(string itemId, int index); + + /// + /// Gets the providers. + /// + /// The item identifier. + /// IEnumerable{SubtitleProviderInfo}. + IEnumerable GetProviders(string itemId); } } diff --git a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs b/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs index 69e92c1f58..e2f6dfc97b 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleResponse.cs +++ b/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; } } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs index e833871298..e781c048b9 100644 --- a/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs +++ b/MediaBrowser.Controller/Subtitles/SubtitleSearchRequest.cs @@ -21,8 +21,11 @@ namespace MediaBrowser.Controller.Subtitles public long? RuntimeTicks { get; set; } public Dictionary ProviderIds { get; set; } + public bool SearchAllProviders { get; set; } + public SubtitleSearchRequest() { + SearchAllProviders = true; ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs index 4371ddae49..07a4b5f605 100644 --- a/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs +++ b/MediaBrowser.Model/Entities/LibraryUpdateInfo.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace MediaBrowser.Model.Entities { diff --git a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs b/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs index 0a4a52cd5f..aa697fee3d 100644 --- a/MediaBrowser.Model/Providers/RemoteSubtitleInfo.cs +++ b/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; } + } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 9041d5a92c..bbbcaeedf6 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/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 FetchAudioInfo(T item, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 5ce53378cb..1cc04ed2a2 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleManager = subtitleManager; } - public async Task ProbeVideo(T item, IDirectoryService directoryService, CancellationToken cancellationToken) + public async Task ProbeVideo(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 /// /// The video. /// The current streams. - private async Task AddExternalSubtitles(Video video, List currentStreams, IDirectoryService directoryService, CancellationToken cancellationToken) + /// The directory service. + /// if set to true [enable subtitle downloading]. + /// The cancellation token. + /// Task. + private async Task AddExternalSubtitles(Video video, List 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)) diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index cf14cfadc2..0df2b24f5f 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/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; diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 2d5445653e..08499a20ba 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/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 subtitleProviders) @@ -38,15 +44,45 @@ namespace MediaBrowser.Providers.Subtitles public async Task> 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(); + } + 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 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 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 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(); + } + + var providers = _subtitleProviders + .Where(i => i.SupportedMediaTypes.Contains(mediaType)) + .ToList(); + + return providers.Select(i => new SubtitleProviderInfo + { + Name = i.Name, + Id = GetProviderId(i.Name) + }); + } } } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 40eeea6517..e6976c54bc 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// /// The library update duration /// - private const int LibraryUpdateDuration = 20000; + private const int LibraryUpdateDuration = 5000; public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger) { diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index 9af858234d..266978d03d 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/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." } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs index fd10c03892..565d83ac36 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/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); diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs index 0edd57d2a8..0407051711 100644 --- a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs +++ b/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 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(); } @@ -35,11 +38,17 @@ namespace MediaBrowser.Server.Implementations.Session } } + private IEnumerable GetActiveSockets() + { + return Sockets + .OrderByDescending(i => i.LastActivityDate) + .Where(i => i.State == WebSocketState.Open); + } + private IWebSocketConnection GetActiveSocket() { - var socket = Sockets - .OrderByDescending(i => i.LastActivityDate) - .FirstOrDefault(i => i.State == WebSocketState.Open); + 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 + return SendMessage(new WebSocketMessage { 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 + return SendMessage(new WebSocketMessage { 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 + return SendMessages(new WebSocketMessage { MessageType = "LibraryChanged", Data = info @@ -92,9 +95,7 @@ namespace MediaBrowser.Server.Implementations.Session /// Task. public Task SendRestartRequiredNotification(CancellationToken cancellationToken) { - var socket = GetActiveSocket(); - - return socket.SendAsync(new WebSocketMessage + return SendMessages(new WebSocketMessage { MessageType = "RestartRequired", Data = _appHost.GetSystemInfo() @@ -111,9 +112,7 @@ namespace MediaBrowser.Server.Implementations.Session /// Task. public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) { - var socket = GetActiveSocket(); - - return socket.SendAsync(new WebSocketMessage + return SendMessages(new WebSocketMessage { MessageType = "UserDataChanged", Data = info @@ -128,9 +127,7 @@ namespace MediaBrowser.Server.Implementations.Session /// Task. public Task SendServerShutdownNotification(CancellationToken cancellationToken) { - var socket = GetActiveSocket(); - - return socket.SendAsync(new WebSocketMessage + return SendMessages(new WebSocketMessage { MessageType = "ServerShuttingDown", Data = string.Empty @@ -145,9 +142,7 @@ namespace MediaBrowser.Server.Implementations.Session /// Task. public Task SendServerRestartNotification(CancellationToken cancellationToken) { - var socket = GetActiveSocket(); - - return socket.SendAsync(new WebSocketMessage + return SendMessages(new WebSocketMessage { 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 + return SendMessage(new WebSocketMessage { 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 + return SendMessages(new WebSocketMessage { 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 + return SendMessages(new WebSocketMessage { 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 + return SendMessages(new WebSocketMessage { MessageType = "PlaybackStopped", Data = sessionInfo }, cancellationToken); } + + private Task SendMessage(WebSocketMessage message, CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(message, cancellationToken); + } + + private Task SendMessages(WebSocketMessage 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); + } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index ed264996b6..831067a9ef 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -537,7 +537,7 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(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)); diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 158d1eccd3..f679206f05 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/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", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index b22a95a376..6aba03efce 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -307,6 +307,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -616,6 +619,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest