diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index d61e8d73ad..f21e69290e 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -131,17 +131,17 @@ namespace MediaBrowser.Api.Playback.Progressive { var args = "-vcodec " + codec; + if (state.EnableMpegtsM2TsMode) + { + args += " -mpegts_m2ts_mode 1"; + } + // See if we can save come cpu cycles by avoiding encoding if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) { return state.VideoStream != null && IsH264(state.VideoStream) ? args + " -bsf h264_mp4toannexb" : args; } - if (state.EnableMpegtsM2TsMode) - { - args += " -mpegts_m2ts_mode 1"; - } - const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))"; args += keyFrameArg; diff --git a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs index 810376f6c6..535e74feee 100644 --- a/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs +++ b/MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs @@ -1,5 +1,7 @@ using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; using MediaBrowser.Model.Configuration; +using System; namespace MediaBrowser.Controller.Configuration { @@ -8,6 +10,11 @@ namespace MediaBrowser.Controller.Configuration /// public interface IServerConfigurationManager : IConfigurationManager { + /// + /// Occurs when [configuration updating]. + /// + event EventHandler> ConfigurationUpdating; + /// /// Gets the application paths. /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 6a3709dda6..6a7557e3a5 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -191,6 +191,7 @@ + diff --git a/MediaBrowser.Controller/Security/IEncryptionManager.cs b/MediaBrowser.Controller/Security/IEncryptionManager.cs new file mode 100644 index 0000000000..bb4f77d83b --- /dev/null +++ b/MediaBrowser.Controller/Security/IEncryptionManager.cs @@ -0,0 +1,20 @@ + +namespace MediaBrowser.Controller.Security +{ + public interface IEncryptionManager + { + /// + /// Encrypts the string. + /// + /// The value. + /// System.String. + string EncryptString(string value); + + /// + /// Decrypts the string. + /// + /// The value. + /// System.String. + string DecryptString(string value); + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 486268e2b6..32b1a7e015 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -323,6 +323,9 @@ namespace MediaBrowser.Model.Configuration public bool DownloadMovieSubtitles { get; set; } public bool DownloadEpisodeSubtitles { get; set; } + public string OpenSubtitlesUsername { get; set; } + public string OpenSubtitlesPasswordHash { get; set; } + public SubtitleOptions() { SubtitleDownloadLanguages = new string[] { }; diff --git a/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs b/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs index 929cccd5ab..f76528c3f4 100644 --- a/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs +++ b/MediaBrowser.Providers/Subtitles/OpenSubtitleDownloader.cs @@ -1,7 +1,9 @@ -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Providers; @@ -16,16 +18,52 @@ using System.Threading.Tasks; namespace MediaBrowser.Providers.Subtitles { - public class OpenSubtitleDownloader : ISubtitleProvider + public class OpenSubtitleDownloader : ISubtitleProvider, IDisposable { private readonly ILogger _logger; private readonly IHttpClient _httpClient; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient) + private readonly IServerConfigurationManager _config; + private readonly IEncryptionManager _encryption; + + public OpenSubtitleDownloader(ILogManager logManager, IHttpClient httpClient, IServerConfigurationManager config, IEncryptionManager encryption) { _logger = logManager.GetLogger(GetType().Name); _httpClient = httpClient; + _config = config; + _encryption = encryption; + + _config.ConfigurationUpdating += _config_ConfigurationUpdating; + } + + private const string PasswordHashPrefix = "h:"; + void _config_ConfigurationUpdating(object sender, GenericEventArgs e) + { + var options = e.Argument.SubtitleOptions; + + if (options != null && + !string.IsNullOrWhiteSpace(options.OpenSubtitlesPasswordHash) && + !options.OpenSubtitlesPasswordHash.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) + { + options.OpenSubtitlesPasswordHash = EncryptPassword(options.OpenSubtitlesPasswordHash); + } + } + + private string EncryptPassword(string password) + { + return PasswordHashPrefix + _encryption.EncryptString(password); + } + + private string DecryptPassword(string password) + { + if (password == null || + !password.StartsWith(PasswordHashPrefix, StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + return _encryption.DecryptString(password.Substring(2)); } public string Name @@ -35,7 +73,16 @@ namespace MediaBrowser.Providers.Subtitles public IEnumerable SupportedMediaTypes { - get { return new[] { SubtitleMediaType.Episode, SubtitleMediaType.Movie }; } + get + { + if (string.IsNullOrWhiteSpace(_config.Configuration.SubtitleOptions.OpenSubtitlesUsername) || + string.IsNullOrWhiteSpace(_config.Configuration.SubtitleOptions.OpenSubtitlesPasswordHash)) + { + return new SubtitleMediaType[] { }; + } + + return new[] { SubtitleMediaType.Episode, SubtitleMediaType.Movie }; + } } public Task GetSubtitles(string id, CancellationToken cancellationToken) @@ -59,7 +106,10 @@ namespace MediaBrowser.Providers.Subtitles var downloadsList = new[] { int.Parse(ossId, _usCulture) }; - var resultDownLoad = OpenSubtitles.DownloadSubtitles(downloadsList); + await Login(cancellationToken).ConfigureAwait(false); + + var resultDownLoad = await OpenSubtitles.DownloadSubtitlesAsync(downloadsList, cancellationToken).ConfigureAwait(false); + if (!(resultDownLoad is MethodResponseSubtitleDownload)) { throw new ApplicationException("Invalid response type"); @@ -77,6 +127,21 @@ namespace MediaBrowser.Providers.Subtitles }; } + private async Task Login(CancellationToken cancellationToken) + { + var options = _config.Configuration.SubtitleOptions ?? new SubtitleOptions(); + + var user = options.OpenSubtitlesUsername ?? string.Empty; + var password = DecryptPassword(options.OpenSubtitlesPasswordHash); + + var loginResponse = await OpenSubtitles.LogInAsync(user, password, "en", cancellationToken).ConfigureAwait(false); + + if (!(loginResponse is MethodResponseLogIn)) + { + throw new UnauthorizedAccessException("Authentication to OpenSubtitles failed."); + } + } + public async Task> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken) { var imdbIdText = request.GetProviderId(MetadataProviders.Imdb); @@ -116,13 +181,7 @@ namespace MediaBrowser.Providers.Subtitles Utilities.HttpClient = _httpClient; OpenSubtitles.SetUserAgent("OS Test User Agent"); - var loginResponse = await OpenSubtitles.LogInAsync("", "", "en", cancellationToken).ConfigureAwait(false); - - if (!(loginResponse is MethodResponseLogIn)) - { - _logger.Debug("Login error"); - return new List(); - } + await Login(cancellationToken).ConfigureAwait(false); var subLanguageId = request.Language; var hash = Utilities.ComputeHash(request.MediaPath); @@ -178,5 +237,10 @@ namespace MediaBrowser.Providers.Subtitles IsHashMatch = i.MovieHash == hasCopy }); } + + public void Dispose() + { + _config.ConfigurationUpdating -= _config_ConfigurationUpdating; + } } } diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs index da38616821..ab79c6a1af 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -1,4 +1,5 @@ using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; using MediaBrowser.Common.Implementations.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -29,6 +30,8 @@ namespace MediaBrowser.Server.Implementations.Configuration UpdateMetadataPath(); } + public event EventHandler> ConfigurationUpdating; + /// /// Gets the type of the configuration. /// @@ -73,8 +76,8 @@ namespace MediaBrowser.Server.Implementations.Configuration /// private void UpdateItemsByNamePath() { - ((ServerApplicationPaths) ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ? - null : + ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ? + null : Configuration.ItemsByNamePath; } @@ -105,13 +108,15 @@ namespace MediaBrowser.Server.Implementations.Configuration /// public override void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration) { - var newConfig = (ServerConfiguration) newConfiguration; + var newConfig = (ServerConfiguration)newConfiguration; ValidateItemByNamePath(newConfig); ValidateTranscodingTempPath(newConfig); ValidatePathSubstitutions(newConfig); ValidateMetadataPath(newConfig); + EventHelper.FireEventIfNotNull(ConfigurationUpdating, this, new GenericEventArgs { Argument = newConfig }, Logger); + base.ReplaceConfiguration(newConfiguration); } diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index c464b2534b..52ddcd795f 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -17,8 +17,6 @@ "PasswordResetConfirmation": "Are you sure you wish to reset the password?", "PasswordSaved": "Password saved.", "PasswordMatchError": "Password and password confirmation must match.", - "OptionOff": "Off", - "OptionOn": "On", "OptionRelease": "Official Release", "OptionBeta": "Beta", "OptionDev": "Dev (Unstable)", diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json index c2e29649fd..d011d45d06 100644 --- a/MediaBrowser.Server.Implementations/Localization/Server/server.json +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -147,10 +147,8 @@ "ScheduledTasksTitle": "Scheduled Tasks", "TabMyPlugins": "My Plugins", "TabCatalog": "Catalog", - "TabUpdates": "Updates", "PluginsTitle": "Plugins", "HeaderAutomaticUpdates": "Automatic Updates", - "HeaderUpdateLevel": "Update Level", "HeaderNowPlaying": "Now Playing", "HeaderLatestAlbums": "Latest Albums", "HeaderLatestSongs": "Latest Songs", @@ -706,5 +704,13 @@ "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." + "OptionReportByteRangeSeekingWhenTranscodingHelp": "This is required for some devices that don't time seek very well.", + "HeaderSubtitleDownloadingHelp": "Media Browser can inspect your video files for missing subtitles, and download them using a subtitle provider such as OpenSubtitles.org.", + "HeaderDownloadSubtitlesFor": "Download subtitles for:", + "LabelRequireExternalSubtitles": "Download even if the video already contains graphical subtitles", + "LabelRequireExternalSubtitlesHelp": "Keeping text versions of subtitles will result in more efficient delivery to mobile clients.", + "TabSubtitles": "Subtitles", + "LabelOpenSubtitlesUsername": "Open Subtitles username:", + "LabelOpenSubtitlesPassword": "Open Subtitles password:", + "LabelAudioLanguagePreferenceHelp": "If empty, the default audio track will be selected, regardless of language." } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 3532ee3703..74b8bf2699 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -64,6 +64,7 @@ + @@ -201,6 +202,7 @@ + diff --git a/MediaBrowser.Server.Implementations/Security/EncryptionManager.cs b/MediaBrowser.Server.Implementations/Security/EncryptionManager.cs new file mode 100644 index 0000000000..73a4e30048 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Security/EncryptionManager.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Security; +using System; +using System.Security.Cryptography; +using System.Text; + +namespace MediaBrowser.Server.Implementations.Security +{ + public class EncryptionManager : IEncryptionManager + { + /// + /// Encrypts the string. + /// + /// The value. + /// System.String. + /// value + public string EncryptString(string value) + { + if (value == null) throw new ArgumentNullException("value"); + + return Encoding.Default.GetString(ProtectedData.Protect(Encoding.Default.GetBytes(value), null, DataProtectionScope.LocalMachine)); + } + + /// + /// Decrypts the string. + /// + /// The value. + /// System.String. + /// value + public string DecryptString(string value) + { + if (value == null) throw new ArgumentNullException("value"); + + return Encoding.Default.GetString(ProtectedData.Unprotect(Encoding.Default.GetBytes(value), null, DataProtectionScope.LocalMachine)); + } + } +} diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index c79d84e5a7..9de3198518 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -29,6 +29,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; @@ -61,6 +62,7 @@ using MediaBrowser.Server.Implementations.Localization; using MediaBrowser.Server.Implementations.MediaEncoder; using MediaBrowser.Server.Implementations.Notifications; using MediaBrowser.Server.Implementations.Persistence; +using MediaBrowser.Server.Implementations.Security; using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.Themes; @@ -533,6 +535,8 @@ namespace MediaBrowser.ServerApplication NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager); RegisterSingleInstance(NotificationManager); + RegisterSingleInstance(new EncryptionManager()); + SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor); RegisterSingleInstance(SubtitleManager); diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 2db493f021..58a0c84b0f 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -585,6 +585,7 @@ namespace MediaBrowser.WebDashboard.Api "medialibrarypage.js", "metadataconfigurationpage.js", "metadataimagespage.js", + "metadatasubtitles.js", "moviegenres.js", "moviecollections.js", "movies.js", @@ -605,7 +606,6 @@ namespace MediaBrowser.WebDashboard.Api "playlist.js", "plugincatalogpage.js", "pluginspage.js", - "pluginupdatespage.js", "remotecontrol.js", "scheduledtaskpage.js", "scheduledtaskspage.js", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 3072413f92..7f9d6919e0 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -328,6 +328,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -658,6 +661,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -1781,16 +1787,6 @@ PreserveNewest - - - PreserveNewest - - - - - PreserveNewest - - PreserveNewest diff --git a/OpenSubtitlesHandler/OpenSubtitles.cs b/OpenSubtitlesHandler/OpenSubtitles.cs index 5353586c85..e810dad69c 100644 --- a/OpenSubtitlesHandler/OpenSubtitles.cs +++ b/OpenSubtitlesHandler/OpenSubtitles.cs @@ -551,6 +551,117 @@ namespace OpenSubtitlesHandler } return new MethodResponseError("Fail", "DownloadSubtitles call failed !"); } + + public static async Task DownloadSubtitlesAsync(int[] subIDS, CancellationToken cancellationToken) + { + if (TOKEN == "") + { + OSHConsole.WriteLine("Can't do this call, 'token' value not set. Please use Log In method first.", DebugCode.Error); + return new MethodResponseError("Fail", "Can't do this call, 'token' value not set. Please use Log In method first."); + } + if (subIDS == null) + { + OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); + return new MethodResponseError("Fail", "No subtitle id passed"); ; + } + if (subIDS.Length == 0) + { + OSHConsole.UpdateLine("No subtitle id passed !!", DebugCode.Error); + return new MethodResponseError("Fail", "No subtitle id passed"); ; + } + // Method call .. + List parms = new List(); + // Add token param + parms.Add(new XmlRpcValueBasic(TOKEN, XmlRpcBasicValueType.String)); + // Add subtitle search parameters. Each one will be like 'array' of structs. + XmlRpcValueArray array = new XmlRpcValueArray(); + foreach (int id in subIDS) + { + array.Values.Add(new XmlRpcValueBasic(id, XmlRpcBasicValueType.Int)); + } + // Add the array to the parameters + parms.Add(array); + // Call ! + XmlRpcMethodCall call = new XmlRpcMethodCall("DownloadSubtitles", parms); + OSHConsole.WriteLine("Sending DownloadSubtitles request to the server ...", DebugCode.Good); + // Send the request to the server + + var httpResponse = await Utilities.SendRequestAsync(XmlRpcGenerator.Generate(call), XML_PRC_USERAGENT, cancellationToken).ConfigureAwait(false); + + string response = Utilities.GetStreamString(httpResponse); + if (!response.Contains("ERROR:")) + { + // No error occur, get and decode the response. + XmlRpcMethodCall[] calls = XmlRpcGenerator.DecodeMethodResponse(response); + if (calls.Length > 0) + { + if (calls[0].Parameters.Count > 0) + { + // We expect Struct of 3 members: + //* the first is status + //* the second is [array of structs, each one includes subtitle file]. + //* the third is [double basic value] represent seconds token by server. + XmlRpcValueStruct mainStruct = (XmlRpcValueStruct)calls[0].Parameters[0]; + // Create the response, we'll need it later + MethodResponseSubtitleDownload R = new MethodResponseSubtitleDownload(); + + // To make sure response is not currepted by server, do it in loop + foreach (XmlRpcStructMember MEMBER in mainStruct.Members) + { + if (MEMBER.Name == "status") + { + R.Status = (string)MEMBER.Data.Data; + OSHConsole.WriteLine("Status= " + R.Status); + } + else if (MEMBER.Name == "seconds") + { + R.Seconds = (double)MEMBER.Data.Data; + OSHConsole.WriteLine("Seconds= " + R.Seconds); + } + else if (MEMBER.Name == "data") + { + if (MEMBER.Data is XmlRpcValueArray) + { + OSHConsole.WriteLine("Download results:"); + XmlRpcValueArray rarray = (XmlRpcValueArray)MEMBER.Data; + foreach (IXmlRpcValue subStruct in rarray.Values) + { + if (subStruct == null) continue; + if (!(subStruct is XmlRpcValueStruct)) continue; + + SubtitleDownloadResult result = new SubtitleDownloadResult(); + foreach (XmlRpcStructMember submember in ((XmlRpcValueStruct)subStruct).Members) + { + // To avoid errors of arranged info or missing ones, let's do it with switch.. + switch (submember.Name) + { + case "idsubtitlefile": result.IdSubtitleFile = (string)submember.Data.Data; break; + case "data": result.Data = (string)submember.Data.Data; break; + } + } + R.Results.Add(result); + OSHConsole.WriteLine("> IDSubtilteFile= " + result.ToString()); + } + } + else// Unknown data ? + { + OSHConsole.WriteLine("Data= " + MEMBER.Data.Data.ToString(), DebugCode.Warning); + } + } + } + // Return the response to user !! + return R; + } + } + } + else + { + OSHConsole.WriteLine(response, DebugCode.Error); + return new MethodResponseError("Fail", response); + } + return new MethodResponseError("Fail", "DownloadSubtitles call failed !"); + } + /// /// Returns comments for subtitles ///