diff options
78 files changed, 2352 insertions, 486 deletions
diff --git a/MediaBrowser.Api/DisplayPreferencesService.cs b/MediaBrowser.Api/DisplayPreferencesService.cs index f22dc9e39..4060b42b4 100644 --- a/MediaBrowser.Api/DisplayPreferencesService.cs +++ b/MediaBrowser.Api/DisplayPreferencesService.cs @@ -12,8 +12,7 @@ namespace MediaBrowser.Api /// <summary> /// Class UpdateDisplayPreferences /// </summary> - [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST")] - [Api(("Updates a user's display preferences for an item"))] + [Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")] public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid { /// <summary> @@ -30,8 +29,7 @@ namespace MediaBrowser.Api public string Client { get; set; } } - [Route("/DisplayPreferences/{Id}", "GET")] - [Api(("Gets a user's display preferences for an item"))] + [Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")] public class GetDisplayPreferences : IReturn<DisplayPreferences> { /// <summary> diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index cb104072b..56f71fc00 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -13,8 +13,7 @@ namespace MediaBrowser.Api /// <summary> /// Class GetDirectoryContents /// </summary> - [Route("/Environment/DirectoryContents", "GET")] - [Api(Description = "Gets the contents of a given directory in the file system")] + [Route("/Environment/DirectoryContents", "GET", Summary = "Gets the contents of a given directory in the file system")] public class GetDirectoryContents : IReturn<List<FileSystemEntryInfo>> { /// <summary> @@ -46,8 +45,7 @@ namespace MediaBrowser.Api public bool IncludeHidden { get; set; } } - [Route("/Environment/NetworkShares", "GET")] - [Api(Description = "Gets shares from a network device")] + [Route("/Environment/NetworkShares", "GET", Summary = "Gets shares from a network device")] public class GetNetworkShares : IReturn<List<FileSystemEntryInfo>> { /// <summary> @@ -61,8 +59,7 @@ namespace MediaBrowser.Api /// <summary> /// Class GetDrives /// </summary> - [Route("/Environment/Drives", "GET")] - [Api(Description = "Gets available drives from the server's file system")] + [Route("/Environment/Drives", "GET", Summary = "Gets available drives from the server's file system")] public class GetDrives : IReturn<List<FileSystemEntryInfo>> { } @@ -70,14 +67,12 @@ namespace MediaBrowser.Api /// <summary> /// Class GetNetworkComputers /// </summary> - [Route("/Environment/NetworkDevices", "GET")] - [Api(Description = "Gets a list of devices on the network")] + [Route("/Environment/NetworkDevices", "GET", Summary = "Gets a list of devices on the network")] public class GetNetworkDevices : IReturn<List<FileSystemEntryInfo>> { } - [Route("/Environment/ParentPath", "GET")] - [Api(Description = "Gets the parent path of a given path")] + [Route("/Environment/ParentPath", "GET", Summary = "Gets the parent path of a given path")] public class GetParentPath : IReturn<string> { /// <summary> diff --git a/MediaBrowser.Api/GamesService.cs b/MediaBrowser.Api/GamesService.cs index e371791f8..ff2771ce1 100644 --- a/MediaBrowser.Api/GamesService.cs +++ b/MediaBrowser.Api/GamesService.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -7,6 +6,7 @@ using MediaBrowser.Model.Dto; using ServiceStack; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; @@ -15,8 +15,7 @@ namespace MediaBrowser.Api /// <summary> /// Class GetSimilarGames /// </summary> - [Route("/Games/{Id}/Similar", "GET")] - [Api(Description = "Finds games similar to a given game.")] + [Route("/Games/{Id}/Similar", "GET", Summary = "Finds games similar to a given game.")] public class GetSimilarGames : BaseGetSimilarItemsFromItem { } @@ -24,8 +23,7 @@ namespace MediaBrowser.Api /// <summary> /// Class GetGameSystemSummaries /// </summary> - [Route("/Games/SystemSummaries", "GET")] - [Api(Description = "Finds games similar to a given game.")] + [Route("/Games/SystemSummaries", "GET", Summary = "Finds games similar to a given game.")] public class GetGameSystemSummaries : IReturn<List<GameSystemSummary>> { /// <summary> @@ -39,8 +37,7 @@ namespace MediaBrowser.Api /// <summary> /// Class GetGameSystemSummaries /// </summary> - [Route("/Games/PlayerIndex", "GET")] - [Api(Description = "Gets an index of players (1-x) and the number of games listed under each")] + [Route("/Games/PlayerIndex", "GET", Summary = "Gets an index of players (1-x) and the number of games listed under each")] public class GetPlayerIndex : IReturn<List<ItemIndex>> { /// <summary> @@ -117,7 +114,7 @@ namespace MediaBrowser.Api } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + public object Get(GetPlayerIndex request) { var games = GetAllLibraryItems(request.UserId, _userManager, _libraryManager) diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 96fe01ab3..abeaba910 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -39,6 +39,9 @@ namespace MediaBrowser.Api.LiveTv /// <value>The limit.</value> [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } + + [ApiMember(Name = "IsFavorite", Description = "Filter by channels that are favorites, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsFavorite { get; set; } } [Route("/LiveTv/Channels/{Id}", "GET", Summary = "Gets a live tv channel")] @@ -290,7 +293,8 @@ namespace MediaBrowser.Api.LiveTv ChannelType = request.Type, UserId = request.UserId, StartIndex = request.StartIndex, - Limit = request.Limit + Limit = request.Limit, + IsFavorite = request.IsFavorite }, CancellationToken.None).Result; diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs index 60270a389..be86090ac 100644 --- a/MediaBrowser.Api/LocalizationService.cs +++ b/MediaBrowser.Api/LocalizationService.cs @@ -32,6 +32,14 @@ namespace MediaBrowser.Api } /// <summary> + /// Class ParentalRatings + /// </summary> + [Route("/Localization/Options", "GET", Summary = "Gets localization options")] + public class GetLocalizationOptions : IReturn<List<LocalizatonOption>> + { + } + + /// <summary> /// Class CulturesService /// </summary> public class LocalizationService : BaseApiService @@ -62,6 +70,13 @@ namespace MediaBrowser.Api return ToOptimizedSerializedResultUsingCache(result); } + public object Get(GetLocalizationOptions request) + { + var result = _localization.GetLocalizationOptions().ToList(); + + return ToOptimizedSerializedResultUsingCache(result); + } + /// <summary> /// Gets the specified request. /// </summary> diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 18559a68d..c03eddf99 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -103,6 +103,7 @@ <Compile Include="Playback\Hls\DynamicHlsService.cs" /> <Compile Include="Playback\Hls\HlsSegmentService.cs" /> <Compile Include="Playback\Hls\VideoHlsService.cs" /> + <Compile Include="Playback\ProgressiveStreamService.cs" /> <Compile Include="Playback\Progressive\AudioService.cs" /> <Compile Include="Playback\Progressive\BaseProgressiveStreamingService.cs" /> <Compile Include="Playback\BaseStreamingService.cs" /> diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index 9b9df3a92..c39811bb6 100644 --- a/MediaBrowser.Api/Music/InstantMixService.cs +++ b/MediaBrowser.Api/Music/InstantMixService.cs @@ -1,9 +1,9 @@ using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Querying; using ServiceStack; -using System; using System.Collections.Generic; using System.Linq; @@ -36,103 +36,74 @@ namespace MediaBrowser.Api.Music public class InstantMixService : BaseApiService { private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; + private readonly IMusicManager _musicManager; - public InstantMixService(IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService) + public InstantMixService(IUserManager userManager, IDtoService dtoService, IMusicManager musicManager) { _userManager = userManager; - _libraryManager = libraryManager; _dtoService = dtoService; + _musicManager = musicManager; } public object Get(GetInstantMixFromSong request) { - var item = _dtoService.GetItemByDtoId(request.Id); + var item = (Audio)_dtoService.GetItemByDtoId(request.Id); - var result = GetInstantMixResult(request, item.Genres); + var user = _userManager.GetUserById(request.UserId.Value); - return ToOptimizedSerializedResultUsingCache(result); + var items = _musicManager.GetInstantMixFromSong(item, user); + + return GetResult(items, user, request); } public object Get(GetInstantMixFromAlbum request) { var album = (MusicAlbum)_dtoService.GetItemByDtoId(request.Id); - var genres = album - .RecursiveChildren - .OfType<Audio>() - .SelectMany(i => i.Genres) - .Concat(album.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, genres); + var items = _musicManager.GetInstantMixFromAlbum(album, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } public object Get(GetInstantMixFromMusicGenre request) { - var genre = GetMusicGenre(request.Name, _libraryManager); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, new[] { genre.Name }); + var items = _musicManager.GetInstantMixFromGenres(new[] { request.Name }, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } public object Get(GetInstantMixFromArtist request) { - var artist = GetArtist(request.Name, _libraryManager); - - var genres = _libraryManager.RootFolder - .RecursiveChildren - .OfType<Audio>() - .Where(i => i.HasArtist(artist.Name)) - .SelectMany(i => i.Genres) - .Concat(artist.Genres) - .Distinct(StringComparer.OrdinalIgnoreCase); + var user = _userManager.GetUserById(request.UserId.Value); - var result = GetInstantMixResult(request, genres); + var items = _musicManager.GetInstantMixFromArtist(request.Name, user); - return ToOptimizedSerializedResultUsingCache(result); + return GetResult(items, user, request); } - private ItemsResult GetInstantMixResult(BaseGetSimilarItems request, IEnumerable<string> genres) + private object GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request) { - var user = request.UserId.HasValue ? _userManager.GetUserById(request.UserId.Value) : null; - var fields = request.GetItemFields().ToList(); - var inputItems = user == null - ? _libraryManager.RootFolder.RecursiveChildren - : user.RootFolder.GetRecursiveChildren(user); - - var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - var limit = request.Limit.HasValue ? request.Limit.Value * 2 : 100; - - var items = inputItems - .OfType<Audio>() - .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey))) - .OrderByDescending(i => i.Item2) - .ThenBy(i => Guid.NewGuid()) - .Select(i => i.Item1) - .Take(limit) - .OrderBy(i => Guid.NewGuid()) - .ToList(); + var list = items.ToList(); var result = new ItemsResult { - TotalRecordCount = items.Count + TotalRecordCount = list.Count }; - var dtos = items.Take(request.Limit ?? items.Count) + var dtos = list.Take(request.Limit ?? list.Count) .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); result.Items = dtos.ToArray(); - return result; + return ToOptimizedResult(result); } } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 519ff7947..5ed77356b 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -835,11 +835,6 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected string GetInputArgument(StreamState state) { - if (state.SendInputOverStandardInput) - { - return "-"; - } - var type = InputType.File; var inputPath = new[] { state.MediaPath }; @@ -898,9 +893,7 @@ namespace MediaBrowser.Api.Playback Arguments = commandLineArgs, WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false, - - RedirectStandardInput = state.SendInputOverStandardInput + ErrorDialog = false }, EnableRaisingEvents = true @@ -933,11 +926,6 @@ namespace MediaBrowser.Api.Playback throw; } - if (state.SendInputOverStandardInput) - { - StreamToStandardInput(process, state); - } - // MUST read both stdout and stderr asynchronously or a deadlock may occurr process.BeginOutputReadLine(); @@ -965,32 +953,6 @@ namespace MediaBrowser.Api.Playback } } - private async void StreamToStandardInput(Process process, StreamState state) - { - try - { - await StreamToStandardInputInternal(process, state).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Logger.Debug("Stream to standard input closed normally."); - } - catch (Exception ex) - { - Logger.ErrorException("Error writing to standard input", ex); - } - } - - private async Task StreamToStandardInputInternal(Process process, StreamState state) - { - state.StandardInputCancellationTokenSource = new CancellationTokenSource(); - - using (var fileStream = FileSystem.GetFileStream(state.MediaPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) - { - await new EndlessStreamCopy().CopyStream(fileStream, process.StandardInput.BaseStream, state.StandardInputCancellationTokenSource.Token).ConfigureAwait(false); - } - } - protected int? GetVideoBitrateParamValue(StreamState state) { var bitrate = state.VideoRequest.VideoBitRate; @@ -1280,22 +1242,12 @@ namespace MediaBrowser.Api.Playback } else if (i == 14) { - if (videoRequest != null) - { - videoRequest.Framerate = int.Parse(val, UsCulture); - } + request.StartTimeTicks = long.Parse(val, UsCulture); } else if (i == 15) { if (videoRequest != null) { - request.StartTimeTicks = long.Parse(val, UsCulture); - } - } - else if (i == 16) - { - if (videoRequest != null) - { videoRequest.Level = val; } } @@ -1315,11 +1267,6 @@ namespace MediaBrowser.Api.Playback ParseParams(request); } - if (request.ThrowDebugError) - { - throw new InvalidOperationException("You asked for a debug error, you got one."); - } - var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager); var url = Request.PathInfo; @@ -1369,8 +1316,6 @@ namespace MediaBrowser.Api.Playback { state.MediaPath = path; state.IsRemote = false; - - state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress; } else if (!string.IsNullOrEmpty(mediaUrl)) { @@ -1378,7 +1323,8 @@ namespace MediaBrowser.Api.Playback state.IsRemote = true; } - //state.RunTimeTicks = recording.RunTimeTicks; + state.RunTimeTicks = recording.RunTimeTicks; + if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsRemote) { await Task.Delay(1000, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 96b36ac7f..1a190b75e 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -276,7 +276,7 @@ namespace MediaBrowser.Api.Playback.Hls ? 0 : ((GetHlsVideoStream)state.VideoRequest).TimeStampOffsetMs; - var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds); + var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture)); var threads = GetNumberOfThreads(state, false); diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index ca206c012..55b311f86 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -17,23 +17,22 @@ namespace MediaBrowser.Api.Playback.Progressive /// <summary> /// Class GetAudioStream /// </summary> - [Route("/Audio/{Id}/stream.mp3", "GET")] - [Route("/Audio/{Id}/stream.wma", "GET")] - [Route("/Audio/{Id}/stream.aac", "GET")] - [Route("/Audio/{Id}/stream.flac", "GET")] - [Route("/Audio/{Id}/stream.ogg", "GET")] - [Route("/Audio/{Id}/stream.oga", "GET")] - [Route("/Audio/{Id}/stream.webm", "GET")] - [Route("/Audio/{Id}/stream", "GET")] - [Route("/Audio/{Id}/stream.mp3", "HEAD")] - [Route("/Audio/{Id}/stream.wma", "HEAD")] - [Route("/Audio/{Id}/stream.aac", "HEAD")] - [Route("/Audio/{Id}/stream.flac", "HEAD")] - [Route("/Audio/{Id}/stream.ogg", "HEAD")] - [Route("/Audio/{Id}/stream.oga", "HEAD")] - [Route("/Audio/{Id}/stream.webm", "HEAD")] - [Route("/Audio/{Id}/stream", "HEAD")] - [Api(Description = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.mp3", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.wma", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.aac", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.flac", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.ogg", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.oga", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.webm", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream", "GET", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.mp3", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.wma", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.aac", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.flac", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.ogg", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.oga", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream.webm", "HEAD", Summary = "Gets an audio stream")] + [Route("/Audio/{Id}/stream", "HEAD", Summary = "Gets an audio stream")] public class GetAudioStream : StreamRequest { @@ -44,7 +43,8 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> public class AudioService : BaseProgressiveStreamingService { - public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager, IHttpClient httpClient, IImageProcessor imageProcessor) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager, httpClient, imageProcessor) + public AudioService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager, IHttpClient httpClient, IImageProcessor imageProcessor) + : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager, httpClient, imageProcessor) { } diff --git a/MediaBrowser.Api/Playback/ProgressiveStreamService.cs b/MediaBrowser.Api/Playback/ProgressiveStreamService.cs new file mode 100644 index 000000000..531f79a22 --- /dev/null +++ b/MediaBrowser.Api/Playback/ProgressiveStreamService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Api.Playback.Progressive; + +namespace MediaBrowser.Api.Playback +{ + //public class GetProgressiveAudioStream : StreamRequest + //{ + + //} + + //public class ProgressiveStreamService : BaseApiService + //{ + // public object Get(GetProgressiveAudioStream request) + // { + // return ProcessRequest(request, false); + // } + + // /// <summary> + // /// Gets the specified request. + // /// </summary> + // /// <param name="request">The request.</param> + // /// <returns>System.Object.</returns> + // public object Head(GetProgressiveAudioStream request) + // { + // return ProcessRequest(request, true); + // } + + // protected object ProcessRequest(StreamRequest request, bool isHeadRequest) + // { + // var state = GetState(request, CancellationToken.None).Result; + + // var responseHeaders = new Dictionary<string, string>(); + + // if (request.Static && state.IsRemote) + // { + // AddDlnaHeaders(state, responseHeaders, true); + + // return GetStaticRemoteStreamResult(state.MediaPath, responseHeaders, isHeadRequest).Result; + // } + + // var outputPath = GetOutputFilePath(state); + // var outputPathExists = File.Exists(outputPath); + + // var isStatic = request.Static || + // (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)); + + // AddDlnaHeaders(state, responseHeaders, isStatic); + + // if (request.Static) + // { + // var contentType = state.GetMimeType(state.MediaPath); + + // return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, FileShare.Read, responseHeaders, isHeadRequest); + // } + + // if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)) + // { + // var contentType = state.GetMimeType(outputPath); + + // return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, FileShare.Read, responseHeaders, isHeadRequest); + // } + + // return GetStreamResult(state, responseHeaders, isHeadRequest).Result; + // } + + //} +} diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 0eb2984fb..add517b5d 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -67,11 +67,6 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceProfileId { get; set; } - - /// <summary> - /// For testing purposes - /// </summary> - public bool ThrowDebugError { get; set; } public string Params { get; set; } } diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index 504d7d921..48285a4b1 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -51,8 +51,6 @@ namespace MediaBrowser.Api.Playback public bool HasMediaStreams { get; set; } - public bool SendInputOverStandardInput { get; set; } - public CancellationTokenSource StandardInputCancellationTokenSource { get; set; } public string LiveTvStreamId { get; set; } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index a787684bb..c5d0a621d 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -20,9 +20,8 @@ namespace MediaBrowser.Api.UserLibrary /// <summary> /// Class GetItems /// </summary> - [Route("/Items", "GET")] - [Route("/Users/{UserId}/Items", "GET")] - [Api(Description = "Gets items based on a query.")] + [Route("/Items", "GET", Summary = "Gets items based on a query.")] + [Route("/Users/{UserId}/Items", "GET", Summary = "Gets items based on a query.")] public class GetItems : BaseItemsRequest, IReturn<ItemsResult> { /// <summary> diff --git a/MediaBrowser.Controller/Dlna/DeviceProfile.cs b/MediaBrowser.Controller/Dlna/DeviceProfile.cs index 5950698fb..c1fc713e4 100644 --- a/MediaBrowser.Controller/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Controller/Dlna/DeviceProfile.cs @@ -3,7 +3,6 @@ using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; using System.Xml.Serialization; namespace MediaBrowser.Controller.Dlna diff --git a/MediaBrowser.Controller/Dlna/TranscodingProfile.cs b/MediaBrowser.Controller/Dlna/TranscodingProfile.cs index 289333aa7..707f0c573 100644 --- a/MediaBrowser.Controller/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Controller/Dlna/TranscodingProfile.cs @@ -18,6 +18,9 @@ namespace MediaBrowser.Controller.Dlna [XmlAttribute("audioCodec")] public string AudioCodec { get; set; } + [XmlAttribute("protocol")] + public string Protocol { get; set; } + [XmlAttribute("estimateContentLength")] public bool EstimateContentLength { get; set; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index a02851a9f..2704959e4 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -86,6 +86,13 @@ namespace MediaBrowser.Controller.Dto ChapterInfoDto GetChapterInfoDto(ChapterInfo chapterInfo, BaseItem item); /// <summary> + /// Gets the media sources. + /// </summary> + /// <param name="item">The item.</param> + /// <returns>List{MediaSourceInfo}.</returns> + List<MediaSourceInfo> GetMediaSources(BaseItem item); + + /// <summary> /// Gets the item by name dto. /// </summary> /// <param name="item">The item.</param> diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs new file mode 100644 index 000000000..192ce2e83 --- /dev/null +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using System.Collections.Generic; + +namespace MediaBrowser.Controller.Library +{ + public interface IMusicManager + { + /// <summary> + /// Gets the instant mix from song. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user); + /// <summary> + /// Gets the instant mix from artist. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromArtist(string name, User user); + /// <summary> + /// Gets the instant mix from album. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user); + /// <summary> + /// Gets the instant mix from genre. + /// </summary> + /// <param name="genres">The genres.</param> + /// <param name="user">The user.</param> + /// <returns>IEnumerable{Audio}.</returns> + IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user); + } +} diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs index edf86cae8..f8cdc6eee 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvRecording.cs @@ -1,8 +1,8 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Library; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.Library; namespace MediaBrowser.Controller.LiveTv { @@ -14,6 +14,8 @@ namespace MediaBrowser.Controller.LiveTv RecordingInfo RecordingInfo { get; set; } + long? RunTimeTicks { get; set; } + string GetClientTypeName(); string GetUserDataKey(); diff --git a/MediaBrowser.Controller/Localization/ILocalizationManager.cs b/MediaBrowser.Controller/Localization/ILocalizationManager.cs index dde2f7878..f41940ed4 100644 --- a/MediaBrowser.Controller/Localization/ILocalizationManager.cs +++ b/MediaBrowser.Controller/Localization/ILocalizationManager.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using System; using System.Collections.Generic; -using System.Threading.Tasks; namespace MediaBrowser.Controller.Localization { @@ -31,5 +31,42 @@ namespace MediaBrowser.Controller.Localization /// <param name="rating">The rating.</param> /// <returns>System.Int32.</returns> int? GetRatingLevel(string rating); + + /// <summary> + /// Gets the localized string. + /// </summary> + /// <param name="phrase">The phrase.</param> + /// <param name="culture">The culture.</param> + /// <returns>System.String.</returns> + string GetLocalizedString(string phrase, string culture); + + /// <summary> + /// Gets the localized string. + /// </summary> + /// <param name="phrase">The phrase.</param> + /// <returns>System.String.</returns> + string GetLocalizedString(string phrase); + + /// <summary> + /// Localizes the document. + /// </summary> + /// <param name="document">The document.</param> + /// <param name="culture">The culture.</param> + /// <param name="tokenBuilder">The token builder.</param> + /// <returns>System.String.</returns> + string LocalizeDocument(string document, string culture, Func<string, string> tokenBuilder); + + /// <summary> + /// Gets the localization options. + /// </summary> + /// <returns>IEnumerable{LocalizatonOption}.</returns> + IEnumerable<LocalizatonOption> GetLocalizationOptions(); + + /// <summary> + /// Gets the java script localization dictionary. + /// </summary> + /// <param name="culture">The culture.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + Dictionary<string, string> GetJavaScriptLocalizationDictionary(string culture); } } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 9915ac044..16834a945 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -134,6 +134,7 @@ <Compile Include="Library\DeleteOptions.cs" /> <Compile Include="Library\ILibraryPostScanTask.cs" /> <Compile Include="Library\IMetadataSaver.cs" /> + <Compile Include="Library\IMusicManager.cs" /> <Compile Include="Library\ItemUpdateType.cs" /> <Compile Include="Library\IUserDataManager.cs" /> <Compile Include="Library\UserDataSaveEventArgs.cs" /> @@ -155,11 +156,14 @@ <Compile Include="LiveTv\SeriesTimerInfo.cs" /> <Compile Include="LiveTv\TimerInfo.cs" /> <Compile Include="Localization\ILocalizationManager.cs" /> + <Compile Include="MediaEncoding\EncodingOptions.cs" /> <Compile Include="MediaEncoding\ChapterImageRefreshOptions.cs" /> + <Compile Include="MediaEncoding\EncodingResult.cs" /> <Compile Include="MediaEncoding\IEncodingManager.cs" /> <Compile Include="MediaEncoding\ImageEncodingOptions.cs" /> <Compile Include="MediaEncoding\IMediaEncoder.cs" /> <Compile Include="MediaEncoding\InternalMediaInfoResult.cs" /> + <Compile Include="MediaEncoding\VideoEncodingOptions.cs" /> <Compile Include="Net\IHasResultFactory.cs" /> <Compile Include="Net\IHttpResultFactory.cs" /> <Compile Include="Net\IHttpServer.cs" /> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs new file mode 100644 index 000000000..74235becd --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingOptions.cs @@ -0,0 +1,79 @@ +using MediaBrowser.Controller.Dlna; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingOptions + { + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public string ItemId { get; set; } + + /// <summary> + /// Gets or sets the media source identifier. + /// </summary> + /// <value>The media source identifier.</value> + public string MediaSourceId { get; set; } + + /// <summary> + /// Gets or sets the device profile. + /// </summary> + /// <value>The device profile.</value> + public DeviceProfile DeviceProfile { get; set; } + + /// <summary> + /// Gets or sets the output path. + /// </summary> + /// <value>The output path.</value> + public string OutputPath { get; set; } + + /// <summary> + /// Gets or sets the container. + /// </summary> + /// <value>The container.</value> + public string Container { get; set; } + + /// <summary> + /// Gets or sets the audio codec. + /// </summary> + /// <value>The audio codec.</value> + public string AudioCodec { get; set; } + + /// <summary> + /// Gets or sets the start time ticks. + /// </summary> + /// <value>The start time ticks.</value> + public long? StartTimeTicks { get; set; } + + /// <summary> + /// Gets or sets the maximum channels. + /// </summary> + /// <value>The maximum channels.</value> + public int? MaxAudioChannels { get; set; } + + /// <summary> + /// Gets or sets the channels. + /// </summary> + /// <value>The channels.</value> + public int? AudioChannels { get; set; } + + /// <summary> + /// Gets or sets the sample rate. + /// </summary> + /// <value>The sample rate.</value> + public int? AudioSampleRate { get; set; } + + /// <summary> + /// Gets or sets the bit rate. + /// </summary> + /// <value>The bit rate.</value> + public int? AudioBitRate { get; set; } + + /// <summary> + /// Gets or sets the maximum audio bit rate. + /// </summary> + /// <value>The maximum audio bit rate.</value> + public int? MaxAudioBitRate { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs b/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs new file mode 100644 index 000000000..75ee90e42 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/EncodingResult.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class EncodingResult + { + public string OutputPath { get; set; } + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs b/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs new file mode 100644 index 000000000..773f0ea46 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/VideoEncodingOptions.cs @@ -0,0 +1,26 @@ + +namespace MediaBrowser.Controller.MediaEncoding +{ + public class VideoEncodingOptions : EncodingOptions + { + public string VideoCodec { get; set; } + + public string VideoProfile { get; set; } + + public double? VideoLevel { get; set; } + + public int? VideoStreamIndex { get; set; } + + public int? AudioStreamIndex { get; set; } + + public int? SubtitleStreamIndex { get; set; } + + public int? MaxWidth { get; set; } + + public int? MaxHeight { get; set; } + + public int? Height { get; set; } + + public int? Width { get; set; } + } +} diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs index 21206af75..02cc875bd 100644 --- a/MediaBrowser.Controller/Session/ISessionController.cs +++ b/MediaBrowser.Controller/Session/ISessionController.cs @@ -60,6 +60,14 @@ namespace MediaBrowser.Controller.Session Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken); /// <summary> + /// Sends the generic command. + /// </summary> + /// <param name="command">The command.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task SendGenericCommand(GenericCommand command, CancellationToken cancellationToken); + + /// <summary> /// Sends the library update info. /// </summary> /// <param name="info">The info.</param> diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index ec9ecb9ef..958188f37 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -20,13 +20,15 @@ namespace MediaBrowser.Dlna private readonly IXmlSerializer _xmlSerializer; private readonly IFileSystem _fileSystem; private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; - public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger) + public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; _appPaths = appPaths; _logger = logger; + _jsonSerializer = jsonSerializer; //DumpProfiles(); } @@ -381,10 +383,66 @@ namespace MediaBrowser.Dlna public void CreateProfile(DeviceProfile profile) { + profile = ReserializeProfile(profile); + + if (string.IsNullOrWhiteSpace(profile.Name)) + { + throw new ArgumentException("Profile is missing Name"); + } + + var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; + var path = Path.Combine(UserProfilesPath, newFilename); + + _xmlSerializer.SerializeToFile(profile, path); } public void UpdateProfile(DeviceProfile profile) { + profile = ReserializeProfile(profile); + + if (string.IsNullOrWhiteSpace(profile.Id)) + { + throw new ArgumentException("Profile is missing Id"); + } + if (string.IsNullOrWhiteSpace(profile.Name)) + { + throw new ArgumentException("Profile is missing Name"); + } + + var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase)); + + if (current.Info.Type == DeviceProfileType.System) + { + throw new ArgumentException("System profiles are readonly"); + } + + var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; + var path = Path.Combine(UserProfilesPath, newFilename); + + if (!string.Equals(path, current.Path, StringComparison.Ordinal)) + { + File.Delete(current.Path); + } + + _xmlSerializer.SerializeToFile(profile, path); + } + + /// <summary> + /// Recreates the object using serialization, to ensure it's not a subclass. + /// If it's a subclass it may not serlialize properly to xml (different root element tag name) + /// </summary> + /// <param name="profile"></param> + /// <returns></returns> + private DeviceProfile ReserializeProfile(DeviceProfile profile) + { + if (profile.GetType() == typeof(DeviceProfile)) + { + return profile; + } + + var json = _jsonSerializer.SerializeToString(profile); + + return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); } class InternalProfileInfo diff --git a/MediaBrowser.Dlna/PlayTo/DlnaController.cs b/MediaBrowser.Dlna/PlayTo/DlnaController.cs index e99c7b50e..0c9f292ad 100644 --- a/MediaBrowser.Dlna/PlayTo/DlnaController.cs +++ b/MediaBrowser.Dlna/PlayTo/DlnaController.cs @@ -619,5 +619,10 @@ namespace MediaBrowser.Dlna.PlayTo _logger.Log(LogSeverity.Debug, "Controller disposed"); } } + + public Task SendGenericCommand(GenericCommand command, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs new file mode 100644 index 000000000..08b7fbe49 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -0,0 +1,91 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class AudioEncoder + { + private readonly string _ffmpegPath; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly IIsoManager _isoManager; + private readonly ILiveTvManager _liveTvManager; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public AudioEncoder(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager) + { + _ffmpegPath = ffmpegPath; + _logger = logger; + _fileSystem = fileSystem; + _appPaths = appPaths; + _isoManager = isoManager; + _liveTvManager = liveTvManager; + } + + public Task BeginEncoding(InternalEncodingTask task) + { + return new FFMpegProcess(_ffmpegPath, _logger, _fileSystem, _appPaths, _isoManager, _liveTvManager).Start(task, GetArguments); + } + + private string GetArguments(InternalEncodingTask task, string mountedPath) + { + var options = task.Request; + + return string.Format("{0} -i {1} {2} -id3v2_version 3 -write_id3v1 1 \"{3}\"", + GetInputModifier(task), + GetInputArgument(task), + GetOutputModifier(task), + options.OutputPath).Trim(); + } + + private string GetInputModifier(InternalEncodingTask task) + { + return EncodingUtils.GetInputModifier(task); + } + + private string GetInputArgument(InternalEncodingTask task) + { + return EncodingUtils.GetInputArgument(new List<string> { task.MediaPath }, task.IsInputRemote); + } + + private string GetOutputModifier(InternalEncodingTask task) + { + var options = task.Request; + + var audioTranscodeParams = new List<string> + { + "-threads " + EncodingUtils.GetNumberOfThreads(task, false).ToString(_usCulture), + "-vn" + }; + + var bitrate = EncodingUtils.GetAudioBitrateParam(task); + + if (bitrate.HasValue) + { + audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(_usCulture)); + } + + var channels = EncodingUtils.GetNumAudioChannelsParam(options, task.AudioStream); + + if (channels.HasValue) + { + audioTranscodeParams.Add("-ac " + channels.Value); + } + + if (options.AudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + options.AudioSampleRate.Value); + } + + return string.Join(" ", audioTranscodeParams.ToArray()); + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs new file mode 100644 index 000000000..79d512dc1 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -0,0 +1,233 @@ +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public static class EncodingUtils + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + public static string GetInputArgument(List<string> inputFiles, bool isRemote) + { + if (isRemote) + { + return GetHttpInputArgument(inputFiles); + } + + return GetConcatInputArgument(inputFiles); + } + + /// <summary> + /// Gets the concat input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <returns>System.String.</returns> + private static string GetConcatInputArgument(List<string> inputFiles) + { + // Get all streams + // If there's more than one we'll need to use the concat command + if (inputFiles.Count > 1) + { + var files = string.Join("|", inputFiles); + + return string.Format("concat:\"{0}\"", files); + } + + // Determine the input path for video files + return GetFileInputArgument(inputFiles[0]); + } + + /// <summary> + /// Gets the file input argument. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>System.String.</returns> + private static string GetFileInputArgument(string path) + { + return string.Format("file:\"{0}\"", path); + } + + /// <summary> + /// Gets the HTTP input argument. + /// </summary> + /// <param name="inputFiles">The input files.</param> + /// <returns>System.String.</returns> + private static string GetHttpInputArgument(IEnumerable<string> inputFiles) + { + var url = inputFiles.First(); + + return string.Format("\"{0}\"", url); + } + + public static string GetAudioInputModifier(InternalEncodingTask options) + { + return GetCommonInputModifier(options); + } + + public static string GetInputModifier(InternalEncodingTask options) + { + var inputModifier = GetCommonInputModifier(options); + + //if (state.VideoRequest != null) + //{ + // inputModifier += " -fflags genpts"; + //} + + //if (!string.IsNullOrEmpty(state.InputVideoCodec)) + //{ + // inputModifier += " -vcodec " + state.InputVideoCodec; + //} + + //if (!string.IsNullOrEmpty(state.InputVideoSync)) + //{ + // inputModifier += " -vsync " + state.InputVideoSync; + //} + + return inputModifier; + } + + private static string GetCommonInputModifier(InternalEncodingTask options) + { + var inputModifier = string.Empty; + + if (options.EnableDebugLogging) + { + inputModifier += "-loglevel debug"; + } + + var probeSize = GetProbeSizeArgument(options.InputVideoType.HasValue && options.InputVideoType.Value == VideoType.Dvd); + inputModifier += " " + probeSize; + inputModifier = inputModifier.Trim(); + + if (!string.IsNullOrWhiteSpace(options.UserAgent)) + { + inputModifier += " -user-agent \"" + options.UserAgent + "\""; + } + + inputModifier += " " + GetFastSeekValue(options.Request); + inputModifier = inputModifier.Trim(); + + if (!string.IsNullOrEmpty(options.InputFormat)) + { + inputModifier += " -f " + options.InputFormat; + } + + if (!string.IsNullOrEmpty(options.InputAudioCodec)) + { + inputModifier += " -acodec " + options.InputAudioCodec; + } + + if (!string.IsNullOrEmpty(options.InputAudioSync)) + { + inputModifier += " -async " + options.InputAudioSync; + } + + if (options.ReadInputAtNativeFramerate) + { + inputModifier += " -re"; + } + + return inputModifier; + } + + private static string GetFastSeekValue(EncodingOptions options) + { + var time = options.StartTimeTicks; + + if (time.HasValue) + { + var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds; + + if (seconds > 0) + { + return string.Format("-ss {0}", seconds.ToString(UsCulture)); + } + } + + return string.Empty; + } + + public static string GetProbeSizeArgument(bool isDvd) + { + return isDvd ? "-probesize 1G -analyzeduration 200M" : string.Empty; + } + + public static int? GetAudioBitrateParam(InternalEncodingTask task) + { + if (task.Request.AudioBitRate.HasValue) + { + // Make sure we don't request a bitrate higher than the source + var currentBitrate = task.AudioStream == null ? task.Request.AudioBitRate.Value : task.AudioStream.BitRate ?? task.Request.AudioBitRate.Value; + + return Math.Min(currentBitrate, task.Request.AudioBitRate.Value); + } + + return null; + } + + /// <summary> + /// Gets the number of audio channels to specify on the command line + /// </summary> + /// <param name="request">The request.</param> + /// <param name="audioStream">The audio stream.</param> + /// <returns>System.Nullable{System.Int32}.</returns> + public static int? GetNumAudioChannelsParam(EncodingOptions request, MediaStream audioStream) + { + if (audioStream != null) + { + if (audioStream.Channels > 2 && string.Equals(request.AudioCodec, "wma", StringComparison.OrdinalIgnoreCase)) + { + // wmav2 currently only supports two channel output + return 2; + } + } + + if (request.MaxAudioChannels.HasValue) + { + if (audioStream != null && audioStream.Channels.HasValue) + { + return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value); + } + + return request.MaxAudioChannels.Value; + } + + return request.AudioChannels; + } + + public static int GetNumberOfThreads(InternalEncodingTask state, bool isWebm) + { + // Use more when this is true. -re will keep cpu usage under control + if (state.ReadInputAtNativeFramerate) + { + if (isWebm) + { + return Math.Max(Environment.ProcessorCount - 1, 1); + } + + return 0; + } + + // Webm: http://www.webmproject.org/docs/encoder-parameters/ + // The decoder will usually automatically use an appropriate number of threads according to how many cores are available but it can only use multiple threads + // for the coefficient data if the encoder selected --token-parts > 0 at encode time. + + switch (state.QualitySetting) + { + case EncodingQuality.HighSpeed: + return 2; + case EncodingQuality.HighQuality: + return 2; + case EncodingQuality.MaxQuality: + return isWebm ? 2 : 0; + default: + throw new Exception("Unrecognized MediaEncodingQuality value."); + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs b/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs new file mode 100644 index 000000000..05733aef0 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/FFMpegProcess.cs @@ -0,0 +1,168 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class FFMpegProcess : IDisposable + { + private readonly string _ffmpegPath; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IApplicationPaths _appPaths; + private readonly IIsoManager _isoManager; + private readonly ILiveTvManager _liveTvManager; + + private Stream _logFileStream; + private InternalEncodingTask _task; + private IIsoMount _isoMount; + + public FFMpegProcess(string ffmpegPath, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths, IIsoManager isoManager, ILiveTvManager liveTvManager) + { + _ffmpegPath = ffmpegPath; + _logger = logger; + _fileSystem = fileSystem; + _appPaths = appPaths; + _isoManager = isoManager; + _liveTvManager = liveTvManager; + } + + public async Task Start(InternalEncodingTask task, Func<InternalEncodingTask,string,string> argumentsFactory) + { + _task = task; + if (!File.Exists(_ffmpegPath)) + { + throw new InvalidOperationException("ffmpeg was not found at " + _ffmpegPath); + } + + Directory.CreateDirectory(Path.GetDirectoryName(task.Request.OutputPath)); + + string mountedPath = null; + if (task.InputVideoType.HasValue && task.InputVideoType == VideoType.Iso && task.IsoType.HasValue) + { + if (_isoManager.CanMount(task.MediaPath)) + { + _isoMount = await _isoManager.Mount(task.MediaPath, CancellationToken.None).ConfigureAwait(false); + mountedPath = _isoMount.MountedPath; + } + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + + // Must consume both stdout and stderr or deadlocks may occur + RedirectStandardOutput = true, + RedirectStandardError = true, + + FileName = _ffmpegPath, + WorkingDirectory = Path.GetDirectoryName(_ffmpegPath), + Arguments = argumentsFactory(task, mountedPath), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + + EnableRaisingEvents = true + }; + + _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "ffmpeg-" + task.Id + ".txt"); + Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); + + // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. + _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); + + process.Exited += process_Exited; + + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.ErrorException("Error starting ffmpeg", ex); + + task.OnError(); + + DisposeLogFileStream(); + + process.Dispose(); + + throw; + } + + task.OnBegin(); + + // MUST read both stdout and stderr asynchronously or a deadlock may occurr + process.BeginOutputReadLine(); + +#pragma warning disable 4014 + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + process.StandardError.BaseStream.CopyToAsync(_logFileStream); +#pragma warning restore 4014 + } + + async void process_Exited(object sender, EventArgs e) + { + var process = (Process)sender; + + if (_isoMount != null) + { + _isoMount.Dispose(); + _isoMount = null; + } + + DisposeLogFileStream(); + + try + { + _logger.Info("FFMpeg exited with code {0} for {1}", process.ExitCode, _task.Request.OutputPath); + } + catch + { + _logger.Info("FFMpeg exited with an error for {0}", _task.Request.OutputPath); + } + + _task.OnCompleted(); + + if (!string.IsNullOrEmpty(_task.LiveTvStreamId)) + { + try + { + await _liveTvManager.CloseLiveStream(_task.LiveTvStreamId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing live tv stream", ex); + } + } + } + + public void Dispose() + { + DisposeLogFileStream(); + } + + private void DisposeLogFileStream() + { + if (_logFileStream != null) + { + _logFileStream.Dispose(); + _logFileStream = null; + } + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs new file mode 100644 index 000000000..826525aef --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTask.cs @@ -0,0 +1,95 @@ +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class InternalEncodingTask + { + public string Id { get; set; } + + public CancellationTokenSource CancellationTokenSource { get; set; } + + public double ProgressPercentage { get; set; } + + public EncodingOptions Request { get; set; } + + public VideoEncodingOptions VideoRequest + { + get { return Request as VideoEncodingOptions; } + } + + public string MediaPath { get; set; } + public List<string> StreamFileNames { get; set; } + public bool IsInputRemote { get; set; } + + public VideoType? InputVideoType { get; set; } + public IsoType? IsoType { get; set; } + public long? InputRunTimeTicks; + + public string AudioSync = "1"; + public string VideoSync = "vfr"; + + public string InputAudioSync { get; set; } + public string InputVideoSync { get; set; } + + public bool DeInterlace { get; set; } + + public bool ReadInputAtNativeFramerate { get; set; } + + public string InputFormat { get; set; } + + public string InputVideoCodec { get; set; } + + public string InputAudioCodec { get; set; } + + public string LiveTvStreamId { get; set; } + + public MediaStream AudioStream { get; set; } + public MediaStream VideoStream { get; set; } + public MediaStream SubtitleStream { get; set; } + public bool HasMediaStreams { get; set; } + + public int SegmentLength = 10; + public int HlsListSize; + + public string MimeType { get; set; } + public string OrgPn { get; set; } + public bool EnableMpegtsM2TsMode { get; set; } + + /// <summary> + /// Gets or sets the user agent. + /// </summary> + /// <value>The user agent.</value> + public string UserAgent { get; set; } + + public EncodingQuality QualitySetting { get; set; } + + public InternalEncodingTask() + { + Id = Guid.NewGuid().ToString("N"); + CancellationTokenSource = new CancellationTokenSource(); + StreamFileNames = new List<string>(); + } + + public bool EnableDebugLogging { get; set; } + + internal void OnBegin() + { + + } + + internal void OnCompleted() + { + + } + + internal void OnError() + { + + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs new file mode 100644 index 000000000..fa9b87906 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/InternalEncodingTaskFactory.cs @@ -0,0 +1,323 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.MediaEncoding.Encoder +{ + public class InternalEncodingTaskFactory + { + private readonly ILibraryManager _libraryManager; + private readonly ILiveTvManager _liveTvManager; + private readonly IItemRepository _itemRepo; + private readonly IServerConfigurationManager _config; + + public InternalEncodingTaskFactory(ILibraryManager libraryManager, ILiveTvManager liveTvManager, IItemRepository itemRepo, IServerConfigurationManager config) + { + _libraryManager = libraryManager; + _liveTvManager = liveTvManager; + _itemRepo = itemRepo; + _config = config; + } + + public async Task<InternalEncodingTask> Create(EncodingOptions request, CancellationToken cancellationToken) + { + ValidateInput(request); + + var state = new InternalEncodingTask + { + Request = request + }; + + var item = string.IsNullOrEmpty(request.MediaSourceId) ? + _libraryManager.GetItemById(new Guid(request.ItemId)) : + _libraryManager.GetItemById(new Guid(request.MediaSourceId)); + + if (item is ILiveTvRecording) + { + var recording = await _liveTvManager.GetInternalRecording(request.ItemId, cancellationToken).ConfigureAwait(false); + + if (string.Equals(recording.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + state.InputVideoType = VideoType.VideoFile; + } + + var path = recording.RecordingInfo.Path; + var mediaUrl = recording.RecordingInfo.Url; + + if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl)) + { + var streamInfo = await _liveTvManager.GetRecordingStream(request.ItemId, cancellationToken).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + path = streamInfo.Path; + mediaUrl = streamInfo.Url; + } + + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + state.MediaPath = path; + state.IsInputRemote = false; + } + else if (!string.IsNullOrEmpty(mediaUrl)) + { + state.MediaPath = mediaUrl; + state.IsInputRemote = true; + } + + state.InputRunTimeTicks = recording.RunTimeTicks; + if (recording.RecordingInfo.Status == RecordingStatus.InProgress && !state.IsInputRemote) + { + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + + state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress; + state.AudioSync = "1000"; + state.DeInterlace = true; + state.InputVideoSync = "-1"; + state.InputAudioSync = "1"; + } + else if (item is LiveTvChannel) + { + var channel = _liveTvManager.GetInternalChannel(request.ItemId); + + if (string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) + { + state.InputVideoType = VideoType.VideoFile; + } + + var streamInfo = await _liveTvManager.GetChannelStream(request.ItemId, cancellationToken).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + if (!string.IsNullOrEmpty(streamInfo.Path) && File.Exists(streamInfo.Path)) + { + state.MediaPath = streamInfo.Path; + state.IsInputRemote = false; + + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + } + else if (!string.IsNullOrEmpty(streamInfo.Url)) + { + state.MediaPath = streamInfo.Url; + state.IsInputRemote = true; + } + + state.ReadInputAtNativeFramerate = true; + state.AudioSync = "1000"; + state.DeInterlace = true; + state.InputVideoSync = "-1"; + state.InputAudioSync = "1"; + } + else + { + state.MediaPath = item.Path; + state.IsInputRemote = item.LocationType == LocationType.Remote; + + var video = item as Video; + + if (video != null) + { + state.InputVideoType = video.VideoType; + state.IsoType = video.IsoType; + + state.StreamFileNames = video.PlayableStreamFileNames.ToList(); + } + + state.InputRunTimeTicks = item.RunTimeTicks; + } + + var videoRequest = request as VideoEncodingOptions; + + var mediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery + { + ItemId = item.Id + + }).ToList(); + + if (videoRequest != null) + { + state.VideoStream = GetMediaStream(mediaStreams, videoRequest.VideoStreamIndex, MediaStreamType.Video); + state.SubtitleStream = GetMediaStream(mediaStreams, videoRequest.SubtitleStreamIndex, MediaStreamType.Subtitle, false); + state.AudioStream = GetMediaStream(mediaStreams, videoRequest.AudioStreamIndex, MediaStreamType.Audio); + + if (state.VideoStream != null && state.VideoStream.IsInterlaced) + { + state.DeInterlace = true; + } + } + else + { + state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); + } + + state.HasMediaStreams = mediaStreams.Count > 0; + + state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10; + state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440; + + state.QualitySetting = GetQualitySetting(); + + ApplyDeviceProfileSettings(state); + + return state; + } + + private void ValidateInput(EncodingOptions request) + { + if (string.IsNullOrWhiteSpace(request.ItemId)) + { + throw new ArgumentException("ItemId is required."); + } + if (string.IsNullOrWhiteSpace(request.OutputPath)) + { + throw new ArgumentException("OutputPath is required."); + } + if (string.IsNullOrWhiteSpace(request.Container)) + { + throw new ArgumentException("Container is required."); + } + if (string.IsNullOrWhiteSpace(request.AudioCodec)) + { + throw new ArgumentException("AudioCodec is required."); + } + + var videoRequest = request as VideoEncodingOptions; + + if (videoRequest == null) + { + return; + } + } + + /// <summary> + /// Determines which stream will be used for playback + /// </summary> + /// <param name="allStream">All stream.</param> + /// <param name="desiredIndex">Index of the desired.</param> + /// <param name="type">The type.</param> + /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param> + /// <returns>MediaStream.</returns> + private MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true) + { + var streams = allStream.Where(s => s.Type == type).OrderBy(i => i.Index).ToList(); + + if (desiredIndex.HasValue) + { + var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value); + + if (stream != null) + { + return stream; + } + } + + if (returnFirstIfNoIndex && type == MediaStreamType.Audio) + { + return streams.FirstOrDefault(i => i.Channels.HasValue && i.Channels.Value > 0) ?? + streams.FirstOrDefault(); + } + + // Just return the first one + return returnFirstIfNoIndex ? streams.FirstOrDefault() : null; + } + + private void ApplyDeviceProfileSettings(InternalEncodingTask state) + { + var profile = state.Request.DeviceProfile; + + if (profile == null) + { + // Don't use settings from the default profile. + // Only use a specific profile if it was requested. + return; + } + + var container = state.Request.Container; + + var audioCodec = state.Request.AudioCodec; + + if (string.Equals(audioCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.AudioStream != null) + { + audioCodec = state.AudioStream.Codec; + } + + var videoCodec = state.VideoRequest == null ? null : state.VideoRequest.VideoCodec; + + if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase) && state.VideoStream != null) + { + videoCodec = state.VideoStream.Codec; + } + + var mediaProfile = state.VideoRequest == null ? + profile.GetAudioMediaProfile(container, audioCodec, state.AudioStream) : + profile.GetVideoMediaProfile(container, audioCodec, videoCodec, state.AudioStream, state.VideoStream); + + if (mediaProfile != null) + { + state.MimeType = mediaProfile.MimeType; + state.OrgPn = mediaProfile.OrgPn; + } + + var transcodingProfile = state.VideoRequest == null ? + profile.GetAudioTranscodingProfile(container, audioCodec) : + profile.GetVideoTranscodingProfile(container, audioCodec, videoCodec); + + if (transcodingProfile != null) + { + //state.EstimateContentLength = transcodingProfile.EstimateContentLength; + state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; + //state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; + + foreach (var setting in transcodingProfile.Settings) + { + switch (setting.Name) + { + case TranscodingSettingType.VideoProfile: + { + if (state.VideoRequest != null && string.IsNullOrWhiteSpace(state.VideoRequest.VideoProfile)) + { + state.VideoRequest.VideoProfile = setting.Value; + } + break; + } + default: + throw new ArgumentException("Unrecognized TranscodingSettingType"); + } + } + } + } + + private EncodingQuality GetQualitySetting() + { + var quality = _config.Configuration.MediaEncodingQuality; + + if (quality == EncodingQuality.Auto) + { + var cpuCount = Environment.ProcessorCount; + + if (cpuCount >= 4) + { + //return EncodingQuality.HighQuality; + } + + return EncodingQuality.HighSpeed; + } + + return quality; + } + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index fac54ecff..93df0c8b9 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -6,10 +6,10 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; -using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -122,35 +122,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <exception cref="System.ArgumentException">Unrecognized InputType</exception> public string GetInputArgument(string[] inputFiles, InputType type) { - string inputPath; - - switch (type) - { - case InputType.Bluray: - case InputType.Dvd: - case InputType.File: - inputPath = GetConcatInputArgument(inputFiles); - break; - case InputType.Url: - inputPath = GetHttpInputArgument(inputFiles); - break; - default: - throw new ArgumentException("Unrecognized InputType"); - } - - return inputPath; - } - - /// <summary> - /// Gets the HTTP input argument. - /// </summary> - /// <param name="inputFiles">The input files.</param> - /// <returns>System.String.</returns> - private string GetHttpInputArgument(string[] inputFiles) - { - var url = inputFiles[0]; - - return string.Format("\"{0}\"", url); + return EncodingUtils.GetInputArgument(inputFiles.ToList(), type == InputType.Url); } /// <summary> @@ -160,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>System.String.</returns> public string GetProbeSizeArgument(InputType type) { - return type == InputType.Dvd ? "-probesize 1G -analyzeduration 200M" : string.Empty; + return EncodingUtils.GetProbeSizeArgument(type == InputType.Dvd); } /// <summary> @@ -879,36 +851,6 @@ namespace MediaBrowser.MediaEncoding.Encoder return memoryStream; } - /// <summary> - /// Gets the file input argument. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>System.String.</returns> - private string GetFileInputArgument(string path) - { - return string.Format("file:\"{0}\"", path); - } - - /// <summary> - /// Gets the concat input argument. - /// </summary> - /// <param name="playableStreamFiles">The playable stream files.</param> - /// <returns>System.String.</returns> - private string GetConcatInputArgument(string[] playableStreamFiles) - { - // Get all streams - // If there's more than one we'll need to use the concat command - if (playableStreamFiles.Length > 1) - { - var files = string.Join("|", playableStreamFiles); - - return string.Format("concat:\"{0}\"", files); - } - - // Determine the input path for video files - return GetFileInputArgument(playableStreamFiles[0]); - } - public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken) { return new ImageEncoder(FFMpegPath, _logger, _fileSystem, _appPaths).EncodeImage(options, cancellationToken); diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index fb1041f89..ee1658ddd 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -48,7 +48,12 @@ </ItemGroup> <ItemGroup> <Compile Include="BdInfo\BdInfoExaminer.cs" /> + <Compile Include="Encoder\AudioEncoder.cs" /> + <Compile Include="Encoder\EncodingUtils.cs" /> + <Compile Include="Encoder\FFMpegProcess.cs" /> <Compile Include="Encoder\ImageEncoder.cs" /> + <Compile Include="Encoder\InternalEncodingTask.cs" /> + <Compile Include="Encoder\InternalEncodingTaskFactory.cs" /> <Compile Include="Encoder\MediaEncoder.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> </ItemGroup> diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 9aefb4f1c..6c4d9d9e2 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -428,6 +428,9 @@ <Compile Include="..\MediaBrowser.Model\Session\BrowseRequest.cs"> <Link>Session\BrowseRequest.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\Session\GenericCommand.cs"> + <Link>Session\GenericCommand.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\Session\MessageCommand.cs"> <Link>Session\MessageCommand.cs</Link> </Compile> diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index ce2a7600f..b39cecc61 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -415,6 +415,9 @@ <Compile Include="..\MediaBrowser.Model\Session\BrowseRequest.cs"> <Link>Session\BrowseRequest.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\Session\GenericCommand.cs"> + <Link>Session\GenericCommand.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\Session\MessageCommand.cs"> <Link>Session\MessageCommand.cs</Link> </Compile> diff --git a/MediaBrowser.Model/ApiClient/IApiClient.cs b/MediaBrowser.Model/ApiClient/IApiClient.cs index 8de54f34a..28c5822e9 100644 --- a/MediaBrowser.Model/ApiClient/IApiClient.cs +++ b/MediaBrowser.Model/ApiClient/IApiClient.cs @@ -590,6 +590,14 @@ namespace MediaBrowser.Model.ApiClient Task SendPlayCommandAsync(string sessionId, PlayRequest request); /// <summary> + /// Sends the command asynchronous. + /// </summary> + /// <param name="sessionId">The session identifier.</param> + /// <param name="request">The request.</param> + /// <returns>Task.</returns> + Task SendCommandAsync(string sessionId, GenericCommand request); + + /// <summary> /// Sends a system command to the client /// </summary> /// <param name="sessionId">The session id.</param> diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index d0caa3ad2..932d5d63d 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -218,6 +218,8 @@ namespace MediaBrowser.Model.Configuration public string ServerName { get; set; } public string WanDdns { get; set; } + public string UICulture { get; set; } + public DlnaOptions DlnaOptions { get; set; } /// <summary> @@ -281,6 +283,8 @@ namespace MediaBrowser.Model.Configuration MetadataOptions = options.ToArray(); DlnaOptions = new DlnaOptions(); + + UICulture = "en-us"; } } diff --git a/MediaBrowser.Model/Globalization/CountryInfo.cs b/MediaBrowser.Model/Globalization/CountryInfo.cs index 16aea8436..9f5f00d80 100644 --- a/MediaBrowser.Model/Globalization/CountryInfo.cs +++ b/MediaBrowser.Model/Globalization/CountryInfo.cs @@ -30,4 +30,10 @@ namespace MediaBrowser.Model.Globalization /// <value>The name of the three letter ISO region.</value> public string ThreeLetterISORegionName { get; set; } } + + public class LocalizatonOption + { + public string Name { get; set; } + public string Value { get; set; } + } } diff --git a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs index d17e2a7f9..fe6faf363 100644 --- a/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/ChannelInfoDto.cs @@ -33,6 +33,8 @@ namespace MediaBrowser.Model.LiveTv /// <value>The external identifier.</value> public string ExternalId { get; set; } + public List<MediaSourceInfo> MediaSources { get; set; } + /// <summary> /// Gets or sets the image tags. /// </summary> @@ -112,6 +114,7 @@ namespace MediaBrowser.Model.LiveTv public ChannelInfoDto() { ImageTags = new Dictionary<ImageType, Guid>(); + MediaSources = new List<MediaSourceInfo>(); } public event PropertyChangedEventHandler PropertyChanged; diff --git a/MediaBrowser.Model/LiveTv/ChannelQuery.cs b/MediaBrowser.Model/LiveTv/ChannelQuery.cs index eb3b20ce3..9079ebacd 100644 --- a/MediaBrowser.Model/LiveTv/ChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/ChannelQuery.cs @@ -13,6 +13,12 @@ namespace MediaBrowser.Model.LiveTv public ChannelType? ChannelType { get; set; } /// <summary> + /// Gets or sets a value indicating whether this instance is favorite. + /// </summary> + /// <value><c>null</c> if [is favorite] contains no value, <c>true</c> if [is favorite]; otherwise, <c>false</c>.</value> + public bool? IsFavorite { get; set; } + + /// <summary> /// Gets or sets the user identifier. /// </summary> /// <value>The user identifier.</value> diff --git a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs index 40aa5710e..de07382c0 100644 --- a/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/RecordingInfoDto.cs @@ -1,11 +1,11 @@ -using System.Diagnostics; -using System.Runtime.Serialization; -using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Library; using System; using System.Collections.Generic; using System.ComponentModel; -using MediaBrowser.Model.Library; +using System.Diagnostics; +using System.Runtime.Serialization; namespace MediaBrowser.Model.LiveTv { @@ -248,10 +248,13 @@ namespace MediaBrowser.Model.LiveTv /// <value>The type.</value> public string Type { get; set; } + public List<MediaSourceInfo> MediaSources { get; set; } + public RecordingInfoDto() { Genres = new List<string>(); ImageTags = new Dictionary<ImageType, Guid>(); + MediaSources = new List<MediaSourceInfo>(); } public event PropertyChangedEventHandler PropertyChanged; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index bf29b4bff..207543fe8 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -132,6 +132,7 @@ <Compile Include="Querying\UserQuery.cs" /> <Compile Include="Search\SearchQuery.cs" /> <Compile Include="Session\BrowseRequest.cs" /> + <Compile Include="Session\GenericCommand.cs" /> <Compile Include="Session\MessageCommand.cs" /> <Compile Include="Session\PlaybackReports.cs" /> <Compile Include="Session\PlayRequest.cs" /> diff --git a/MediaBrowser.Model/Session/GenericCommand.cs b/MediaBrowser.Model/Session/GenericCommand.cs new file mode 100644 index 000000000..f7ea0a84a --- /dev/null +++ b/MediaBrowser.Model/Session/GenericCommand.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Model.Session +{ + public class GenericCommand + { + public string Name { get; set; } + + public string ControllingUserId { get; set; } + + public Dictionary<string, string> Arguments { get; set; } + + public GenericCommand() + { + Arguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + } + + /// <summary> + /// This exists simply to identify a set of known commands. + /// </summary> + public enum CoreGenericCommand + { + MoveUp = 0, + MoveDown = 1, + MoveLeft = 2, + MoveRight = 3, + PageUp = 4, + PageDown = 5, + PreviousLetter = 6, + NextLetter = 7, + ToggleOsd = 8, + ToggleContextMenu = 9, + Select = 10, + Back = 11, + TakeScreenshot = 12, + SendKey = 13, + SendString = 14, + GoHome = 15, + GoToSettings = 16, + VolumeUp = 17, + VolumeDown = 18, + Mute = 19, + Unmute = 20, + ToggleMute = 21 + } +} diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index 949274a5d..74d7a70a3 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -39,14 +39,22 @@ namespace MediaBrowser.Model.Session /// <summary> /// The play now /// </summary> - PlayNow, + PlayNow = 0, /// <summary> /// The play next /// </summary> - PlayNext, + PlayNext = 1, /// <summary> /// The play last /// </summary> - PlayLast + PlayLast = 2, + /// <summary> + /// The play instant mix + /// </summary> + PlayInstantMix = 3, + /// <summary> + /// The play shuffle + /// </summary> + PlayShuffle = 4 } }
\ No newline at end of file diff --git a/MediaBrowser.Model/Session/PlaystateCommand.cs b/MediaBrowser.Model/Session/PlaystateCommand.cs index d83c6dae5..91572ba62 100644 --- a/MediaBrowser.Model/Session/PlaystateCommand.cs +++ b/MediaBrowser.Model/Session/PlaystateCommand.cs @@ -33,7 +33,15 @@ namespace MediaBrowser.Model.Session /// <summary> /// The fullscreen /// </summary> - Fullscreen + Fullscreen, + /// <summary> + /// The rewind + /// </summary> + Rewind, + /// <summary> + /// The fast forward + /// </summary> + FastForward } public class PlaystateRequest diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 1833b708f..02da776bd 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; @@ -1064,7 +1065,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary); } - dto.MediaSources = GetMediaSources(audio); + dto.MediaSources = GetAudioMediaSources(audio); dto.MediaSourceCount = 1; } @@ -1100,7 +1101,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.MediaSources)) { - dto.MediaSources = GetMediaSources(video); + dto.MediaSources = GetVideoMediaSources(video); } if (fields.Contains(ItemFields.Chapters)) @@ -1266,9 +1267,48 @@ namespace MediaBrowser.Server.Implementations.Dto { SetBookProperties(dto, book); } + + var tvChannel = item as LiveTvChannel; + + if (tvChannel != null) + { + dto.MediaSources = GetMediaSources(tvChannel); + } + } + + public List<MediaSourceInfo> GetMediaSources(BaseItem item) + { + var video = item as Video; + + if (video != null) + { + return GetVideoMediaSources(video); + } + + var audio = item as Audio; + + if (audio != null) + { + return GetAudioMediaSources(audio); + } + + var result = new List<MediaSourceInfo> + { + new MediaSourceInfo + { + Id = item.Id.ToString("N"), + LocationType = item.LocationType, + Name = item.Name, + Path = GetMappedPath(item), + MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { ItemId = item.Id }).ToList(), + RunTimeTicks = item.RunTimeTicks + } + }; + + return result; } - private List<MediaSourceInfo> GetMediaSources(Video item) + private List<MediaSourceInfo> GetVideoMediaSources(Video item) { var result = item.GetAlternateVersions().Select(GetVersionInfo).ToList(); @@ -1293,11 +1333,11 @@ namespace MediaBrowser.Server.Implementations.Dto .ToList(); } - private List<MediaSourceInfo> GetMediaSources(Audio item) + private List<MediaSourceInfo> GetAudioMediaSources(Audio item) { var result = new List<MediaSourceInfo> { - GetVersionInfo(item, true) + GetVersionInfo(item) }; return result; @@ -1321,7 +1361,7 @@ namespace MediaBrowser.Server.Implementations.Dto }; } - private MediaSourceInfo GetVersionInfo(Audio i, bool isPrimary) + private MediaSourceInfo GetVersionInfo(Audio i) { return new MediaSourceInfo { diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 9279fd8d7..5b65a60c8 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -257,8 +257,12 @@ namespace MediaBrowser.Server.Implementations.IO InternalBufferSize = 32767 }; - newWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.DirectoryName | - NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size; + newWatcher.NotifyFilter = NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size | + NotifyFilters.Attributes; newWatcher.Created += watcher_Changed; newWatcher.Deleted += watcher_Changed; @@ -349,13 +353,13 @@ namespace MediaBrowser.Server.Implementations.IO { try { - Logger.Debug("Watcher sees change of type " + e.ChangeType + " to " + e.FullPath); + Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); ReportFileSystemChanged(e.FullPath); } catch (Exception ex) { - Logger.ErrorException("Exception in watcher changed. Path: {0}", ex, e.FullPath); + Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath); } } @@ -397,14 +401,6 @@ namespace MediaBrowser.Server.Implementations.IO Logger.Debug("Ignoring change to {0}", path); return true; } - - // Go up another level - parent = Path.GetDirectoryName(i); - if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) - { - Logger.Debug("Ignoring change to {0}", path); - return true; - } } return false; diff --git a/MediaBrowser.Server.Implementations/Library/MusicManager.cs b/MediaBrowser.Server.Implementations/Library/MusicManager.cs new file mode 100644 index 000000000..9d5826454 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Library/MusicManager.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.Library +{ + public class MusicManager : IMusicManager + { + private readonly ILibraryManager _libraryManager; + + public MusicManager(ILibraryManager libraryManager) + { + _libraryManager = libraryManager; + } + + public IEnumerable<Audio> GetInstantMixFromSong(Audio item, User user) + { + return GetInstantMixFromGenres(item.Genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromArtist(string name, User user) + { + var artist = _libraryManager.GetArtist(name); + + var genres = _libraryManager.RootFolder + .RecursiveChildren + .OfType<Audio>() + .Where(i => i.HasArtist(name)) + .SelectMany(i => i.Genres) + .Concat(artist.Genres) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return GetInstantMixFromGenres(genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user) + { + var genres = item + .RecursiveChildren + .OfType<Audio>() + .SelectMany(i => i.Genres) + .Concat(item.Genres) + .Distinct(StringComparer.OrdinalIgnoreCase); + + return GetInstantMixFromGenres(genres, user); + } + + public IEnumerable<Audio> GetInstantMixFromGenres(IEnumerable<string> genres, User user) + { + var inputItems = user.RootFolder.GetRecursiveChildren(user); + + var genresDictionary = genres.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + + return inputItems + .OfType<Audio>() + .Select(i => new Tuple<Audio, int>(i, i.Genres.Count(genresDictionary.ContainsKey))) + .OrderByDescending(i => i.Item2) + .ThenBy(i => Guid.NewGuid()) + .Select(i => i.Item1) + .Take(100) + .OrderBy(i => Guid.NewGuid()); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 67c011a1f..b92e82385 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -34,8 +34,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio { var collectionType = args.GetCollectionType(); - if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) || - string.IsNullOrEmpty(collectionType)) + if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)) { return new Controller.Entities.Audio.Audio(); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs index 4e1bd860b..cf7358cce 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -222,13 +222,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv RunTimeTicks = (info.EndDate - info.StartDate).Ticks, OriginalAirDate = info.OriginalAirDate, - MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery - { - ItemId = recording.Id - - }).ToList() + MediaSources = _dtoService.GetMediaSources((BaseItem)recording) }; + dto.MediaStreams = dto.MediaSources.SelectMany(i => i.MediaStreams).ToList(); + if (info.Status == RecordingStatus.InProgress) { var now = DateTime.UtcNow.Ticks; @@ -317,7 +315,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv Type = info.GetClientTypeName(), Id = info.Id.ToString("N"), MediaType = info.MediaType, - ExternalId = info.ExternalId + ExternalId = info.ExternalId, + MediaSources = _dtoService.GetMediaSources(info) }; if (user != null) diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index db8786d62..b828dc0de 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -134,6 +134,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv return number; }); + + if (query.IsFavorite.HasValue) + { + var val = query.IsFavorite.Value; + + channels = channels + .Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite == val); + } } channels = channels.OrderBy(i => diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/en_US.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/en_US.json new file mode 100644 index 000000000..45f0d7e13 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/en_US.json @@ -0,0 +1,21 @@ +{ + "SettingsSaved": "Settings saved.", + "AddUser": "Add User", + "Users": "Users", + "Delete": "Delete", + "Administrator": "Administrator", + "Password": "Password", + "CreatePassword": "Create Password", + "DeleteImage": "Delete Image", + "DeleteImageConfirmation": "Are you sure you wish to delete this image?", + "FileReadCancelled": "The file read has been cancelled.", + "FileNotFound": "File not found.", + "FileReadError": "An error occurred while reading the file.", + "DeleteUser": "Delete User", + "DeleteUserConfirmation": "Are you sure you wish to delete {0}?", + "PasswordResetHeader": "Password Reset", + "PasswordResetComplete": "The password has been reset.", + "PasswordResetConfirmation": "Are you sure you wish to reset the password?", + "PasswordSaved": "Password saved.", + "PasswordMatchError": "Password and password confirmation must match." +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json new file mode 100644 index 000000000..41f8508a5 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -0,0 +1,21 @@ +{ + "SettingsSaved": "Settings saved.", + "AddUser": "Add User", + "Users": "Users", + "Delete": "Delete", + "Administrator": "Administrator", + "Password": "Password", + "CreatePassword": "Create Password", + "DeleteImage": "Delete Image", + "DeleteImageConfirmation": "Are you sure you wish to delete this image?", + "FileReadCancelled": "The file read has been cancelled.", + "FileNotFound": "File not found.", + "FileReadError": "An error occurred while reading the file.", + "DeleteUser": "Delete User", + "DeleteUserConfirmation": "Are you sure you wish to delete {0}?", + "PasswordResetHeader": "Password Reset", + "PasswordResetComplete": "The password has been reset.", + "PasswordResetConfirmation": "Are you sure you wish to reset the password?", + "PasswordSaved": "Password saved.", + "PasswordMatchError": "Password and password confirmation must match." +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/pt_PT.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/pt_PT.json new file mode 100644 index 000000000..25d3ff944 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/pt_PT.json @@ -0,0 +1,21 @@ +{ + "SettingsSaved": "Configura\u00e7\u00f5es guardadas.", + "AddUser": "Adicionar Utilizador", + "Users": "Utilizadores", + "Delete": "Apagar", + "Administrator": "Administrador", + "Password": "Senha", + "CreatePassword": "Criar Senha", + "DeleteImage": "Apagar Imagem", + "DeleteImageConfirmation": "Tem a certeza que pretende apagar a imagem?", + "FileReadCancelled": "The file read has been cancelled.", + "FileNotFound": "Ficheiro n\u00e3o encontrado", + "FileReadError": "Ocorreu um erro ao ler o ficheiro.", + "DeleteUser": "Apagar Utilizador", + "DeleteUserConfirmation": "Tem a certeza que pretende apagar {0}?", + "PasswordResetHeader": "Password Reset", + "PasswordResetComplete": "The password has been reset.", + "PasswordResetConfirmation": "Are you sure you wish to reset the password?", + "PasswordSaved": "Senha guardada.", + "PasswordMatchError": "Password and password confirmation must match." +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs index 0d428742c..6f39c6759 100644 --- a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs +++ b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs @@ -1,9 +1,9 @@ using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Localization; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Serialization; using MoreLinq; using System; using System.Collections.Concurrent; @@ -11,6 +11,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; +using MediaBrowser.Common.Extensions; namespace MediaBrowser.Server.Implementations.Localization { @@ -33,15 +35,17 @@ namespace MediaBrowser.Server.Implementations.Localization new ConcurrentDictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); private readonly IFileSystem _fileSystem; - + private readonly IJsonSerializer _jsonSerializer; + /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager"/> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> - public LocalizationManager(IServerConfigurationManager configurationManager, IFileSystem fileSystem) + public LocalizationManager(IServerConfigurationManager configurationManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer) { _configurationManager = configurationManager; _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; ExtractAll(); } @@ -241,5 +245,112 @@ namespace MediaBrowser.Server.Implementations.Localization return value == null ? (int?)null : value.Value; } + + public string GetLocalizedString(string phrase) + { + return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture); + } + + public string GetLocalizedString(string phrase, string culture) + { + var dictionary = GetLocalizationDictionary(culture); + + string value; + + if (dictionary.TryGetValue(phrase, out value)) + { + return value; + } + + return phrase; + } + + private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = + new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); + + public Dictionary<string, string> GetLocalizationDictionary(string culture) + { + const string prefix = "Server"; + var key = prefix + culture; + + return _dictionaries.GetOrAdd(key, k => GetDictionary(prefix, culture, "server.json")); + } + + public Dictionary<string, string> GetJavaScriptLocalizationDictionary(string culture) + { + const string prefix = "JavaScript"; + var key = prefix + culture; + + return _dictionaries.GetOrAdd(key, k => GetDictionary(prefix, culture, "javascript.json")); + } + + private Dictionary<string, string> GetDictionary(string prefix, string culture, string baseFilename) + { + var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + var assembly = GetType().Assembly; + var namespaceName = GetType().Namespace + "." + prefix; + + CopyInto(dictionary, namespaceName + "." + baseFilename, assembly); + CopyInto(dictionary, namespaceName + "." + GetResourceFilename(culture), assembly); + + return dictionary; + } + + private void CopyInto(IDictionary<string, string> dictionary, string resourcePath, Assembly assembly) + { + using (var stream = assembly.GetManifestResourceStream(resourcePath)) + { + if (stream != null) + { + var dict = _jsonSerializer.DeserializeFromStream<Dictionary<string, string>>(stream); + + foreach (var key in dict.Keys) + { + dictionary[key] = dict[key]; + } + } + } + } + + private string GetResourceFilename(string culture) + { + var parts = culture.Split('-'); + + if (parts.Length == 2) + { + culture = parts[0].ToLower() + "_" + parts[1].ToUpper(); + } + else + { + culture = culture.ToLower(); + } + + return culture + ".json"; + } + + public IEnumerable<LocalizatonOption> GetLocalizationOptions() + { + return new List<LocalizatonOption> + { + new LocalizatonOption{ Name="English (United States)", Value="en-us"}, + new LocalizatonOption{ Name="German", Value="de"}, + new LocalizatonOption{ Name="Portuguese (Portugal)", Value="pt-PT"}, + new LocalizatonOption{ Name="Russian", Value="ru"} + + }.OrderBy(i => i.Name); + } + + public string LocalizeDocument(string document, string culture, Func<string,string> tokenBuilder) + { + foreach (var pair in GetLocalizationDictionary(culture).ToList()) + { + var token = tokenBuilder(pair.Key); + + document = document.Replace(token, pair.Value, StringComparison.Ordinal); + } + + return document; + } } } diff --git a/MediaBrowser.Server.Implementations/Localization/Server/de.json b/MediaBrowser.Server.Implementations/Localization/Server/de.json new file mode 100644 index 000000000..ab3271b50 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Server/de.json @@ -0,0 +1,17 @@ +{ + "LabelExit": "Ende", + "LabelVisitCommunity": "Besuche die Community", + "LabelGithubWiki": "Github Wiki", + "LabelSwagger": "Swagger", + "LabelStandard": "Standard", + "LabelViewApiDocumentation": "Zeige Api Dokumentation", + "LabelBrowseLibrary": "Durchsuche Bibliothek", + "LabelConfigureMediaBrowser": "Konfiguriere Media Browser", + "LabelOpenLibraryViewer": "\u00d6ffne Bibliothekenansicht", + "LabelRestartServer": "Server neustarten", + "LabelShowLogWindow": "Zeige Log Fenster", + "LabelPrevious": "Vorheriges", + "LabelFinish": "Ende", + "LabelNext": "N\u00e4chstes", + "LabelYoureDone": "Du bist fertig!" +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Server/en_US.json b/MediaBrowser.Server.Implementations/Localization/Server/en_US.json new file mode 100644 index 000000000..f74cca1eb --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Server/en_US.json @@ -0,0 +1,17 @@ +{ + "LabelExit": "Exit", + "LabelVisitCommunity": "Visit Community", + "LabelGithubWiki": "Github Wiki", + "LabelSwagger": "Swagger", + "LabelStandard": "Standard", + "LabelViewApiDocumentation": "View Api Documentation", + "LabelBrowseLibrary": "Browse Library", + "LabelConfigureMediaBrowser": "Configure Media Browser", + "LabelOpenLibraryViewer": "Open Library Viewer", + "LabelRestartServer": "Restart Server", + "LabelShowLogWindow": "Show Log Window", + "LabelPrevious": "Previous", + "LabelFinish": "Finish", + "LabelNext": "Next", + "LabelYoureDone": "You're Done!" +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json b/MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json new file mode 100644 index 000000000..700314708 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Server/pt_PT.json @@ -0,0 +1,17 @@ +{ + "LabelExit": "Sair", + "LabelVisitCommunity": "Visitar a Comunidade", + "LabelGithubWiki": "Wiki Github", + "LabelSwagger": "Swagger", + "LabelStandard": "Padr\u00e3o", + "LabelViewApiDocumentation": "Ver Documenta\u00e7\u00e3o da API", + "LabelBrowseLibrary": "Navegar na Biblioteca", + "LabelConfigureMediaBrowser": "Configurar Media Browser", + "LabelOpenLibraryViewer": "Abrir Visualizador da Biblioteca", + "LabelRestartServer": "Reiniciar Servidor", + "LabelShowLogWindow": "Mostrar Janela de Log", + "LabelPrevious": "Anterior", + "LabelFinish": "Terminar", + "LabelNext": "Seguinte", + "LabelYoureDone": "Concluiu!" +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Server/ru.json b/MediaBrowser.Server.Implementations/Localization/Server/ru.json new file mode 100644 index 000000000..4cc6b90f7 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Server/ru.json @@ -0,0 +1,17 @@ +{ + "LabelExit": "\u0412\u044b\u0445\u043e\u0434", + "LabelVisitCommunity": "\u041f\u043e\u0441\u0435\u0442\u0438\u0442\u044c \u0421\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e", + "LabelGithubWiki": "\u0412\u0438\u043a\u0438 \u043d\u0430 Github", + "LabelSwagger": "\u041e\u0444\u043e\u0440\u043c\u043b\u0435\u043d\u0438\u0435", + "LabelStandard": "\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439", + "LabelViewApiDocumentation": "\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043f\u043e API", + "LabelBrowseLibrary": "\u041e\u0431\u043e\u0437\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u041c\u0435\u0434\u0438\u0430\u0442\u0435\u043a\u0438", + "LabelConfigureMediaBrowser": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Media Browser", + "LabelOpenLibraryViewer": "\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u041c\u0435\u0434\u0438\u0430\u0442\u0435\u043a\u0438", + "LabelRestartServer": "\u041f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440", + "LabelShowLogWindow": "\u041e\u043a\u043d\u043e \u0416\u0443\u0440\u043d\u0430\u043b\u0430", + "LabelPrevious": "\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0435", + "LabelFinish": "\u0417\u0430\u043a\u043e\u043d\u0447\u0438\u0442\u044c", + "LabelNext": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435", + "LabelYoureDone": "\u0412\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043b\u0438!" +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Server/server.json b/MediaBrowser.Server.Implementations/Localization/Server/server.json new file mode 100644 index 000000000..62a418391 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Localization/Server/server.json @@ -0,0 +1,17 @@ +{ + "LabelExit": "Exit", + "LabelVisitCommunity": "Visit Community", + "LabelGithubWiki": "Github Wiki", + "LabelSwagger": "Swagger", + "LabelStandard": "Standard", + "LabelViewApiDocumentation": "View Api Documentation", + "LabelBrowseLibrary": "Browse Library", + "LabelConfigureMediaBrowser": "Configure Media Browser", + "LabelOpenLibraryViewer": "Open Library Viewer", + "LabelRestartServer": "Restart Server", + "LabelShowLogWindow": "Show Log Window", + "LabelPrevious": "Previous", + "LabelFinish": "Finish", + "LabelNext": "Next", + "LabelYoureDone": "You're Done!" +}
\ 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 ea7ef2ed6..539b968c7 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -141,6 +141,7 @@ <Compile Include="IO\LibraryMonitor.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> <Compile Include="Library\LibraryManager.cs" /> + <Compile Include="Library\MusicManager.cs" /> <Compile Include="Library\Resolvers\PhotoResolver.cs" /> <Compile Include="Library\SearchEngine.cs" /> <Compile Include="Library\ResolverHelper.cs" /> @@ -281,6 +282,14 @@ <EmbeddedResource Include="Localization\Ratings\ru.txt" /> </ItemGroup> <ItemGroup> + <EmbeddedResource Include="Localization\JavaScript\javascript.json" /> + <EmbeddedResource Include="Localization\Server\server.json" /> + <EmbeddedResource Include="Localization\Server\de.json" /> + <EmbeddedResource Include="Localization\Server\pt_PT.json" /> + <EmbeddedResource Include="Localization\Server\ru.json" /> + <EmbeddedResource Include="Localization\JavaScript\en_US.json" /> + <EmbeddedResource Include="Localization\JavaScript\pt_PT.json" /> + <EmbeddedResource Include="Localization\Server\en_US.json" /> <None Include="packages.config" /> </ItemGroup> <ItemGroup> @@ -385,6 +394,7 @@ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(SolutionDir)\.nuget\nuget.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. diff --git a/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs b/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs index 0e2f9e1b5..7c8d71b4f 100644 --- a/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs +++ b/MediaBrowser.Server.Implementations/Roku/RokuSessionController.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.Server.Implementations.Roku public bool SupportsMediaRemoteControl { - get { return true; } + get { return false; } } public bool IsSessionActive @@ -146,5 +146,16 @@ namespace MediaBrowser.Server.Implementations.Roku RequestContentType = "application/json" }); } + + + public Task SendGenericCommand(GenericCommand command, CancellationToken cancellationToken) + { + return SendCommand(new WebSocketMessage<GenericCommand> + { + MessageType = "Command", + Data = command + + }, cancellationToken); + } } } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 9d405a175..00d2aa992 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -41,6 +41,7 @@ namespace MediaBrowser.Server.Implementations.Session private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; + private readonly IMusicManager _musicManager; /// <summary> /// Gets or sets the configuration manager. @@ -564,7 +565,7 @@ namespace MediaBrowser.Server.Implementations.Session return playedToCompletion; } - + /// <summary> /// Updates playstate position for an item but does not save /// </summary> @@ -666,7 +667,7 @@ namespace MediaBrowser.Server.Implementations.Session var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - + return session.SessionController.SendSystemCommand(command, cancellationToken); } @@ -676,7 +677,7 @@ namespace MediaBrowser.Server.Implementations.Session var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - + return session.SessionController.SendMessageCommand(command, cancellationToken); } @@ -684,14 +685,35 @@ namespace MediaBrowser.Server.Implementations.Session { var session = GetSessionForRemoteControl(sessionId); - var items = command.ItemIds.Select(i => _libraryManager.GetItemById(new Guid(i))) - .Where(i => i.LocationType != LocationType.Virtual) - .ToList(); + var user = session.UserId.HasValue ? _userManager.GetUserById(session.UserId.Value) : null; - if (session.UserId.HasValue) + List<BaseItem> items; + + if (command.PlayCommand == PlayCommand.PlayInstantMix) { - var user = _userManager.GetUserById(session.UserId.Value); + items = command.ItemIds.SelectMany(i => TranslateItemForInstantMix(i, user)) + .Where(i => i.LocationType != LocationType.Virtual) + .ToList(); + command.PlayCommand = PlayCommand.PlayNow; + } + else + { + items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user)) + .Where(i => i.LocationType != LocationType.Virtual) + .ToList(); + } + + if (command.PlayCommand == PlayCommand.PlayShuffle) + { + items = items.OrderBy(i => Guid.NewGuid()).ToList(); + command.PlayCommand = PlayCommand.PlayNow; + } + + command.ItemIds = items.Select(i => i.Id.ToString("N")).ToArray(); + + if (user != null) + { if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full)) { throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name)); @@ -723,13 +745,69 @@ namespace MediaBrowser.Server.Implementations.Session return session.SessionController.SendPlayCommand(command, cancellationToken); } + private IEnumerable<BaseItem> TranslateItemForPlayback(string id, User user) + { + var item = _libraryManager.GetItemById(new Guid(id)); + + if (item.IsFolder) + { + var folder = (Folder)item; + + var items = user == null ? folder.RecursiveChildren : + folder.GetRecursiveChildren(user); + + items = items.Where(i => !i.IsFolder); + + items = items.OrderBy(i => i.SortName); + + return items; + } + + return new[] { item }; + } + + private IEnumerable<BaseItem> TranslateItemForInstantMix(string id, User user) + { + var item = _libraryManager.GetItemById(new Guid(id)); + + var audio = item as Audio; + + if (audio != null) + { + return _musicManager.GetInstantMixFromSong(audio, user); + } + + var artist = item as MusicArtist; + + if (artist != null) + { + return _musicManager.GetInstantMixFromArtist(artist.Name, user); + } + + var album = item as MusicAlbum; + + if (album != null) + { + return _musicManager.GetInstantMixFromAlbum(album, user); + } + + var genre = item as MusicGenre; + + if (genre != null) + { + return _musicManager.GetInstantMixFromGenres(new[] { genre.Name }, user); + } + + return new BaseItem[] { }; + } + public Task SendBrowseCommand(Guid controllingSessionId, Guid sessionId, BrowseRequest command, CancellationToken cancellationToken) { var session = GetSessionForRemoteControl(sessionId); var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - + return session.SessionController.SendBrowseCommand(command, cancellationToken); } diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs index 70d7ac071..ddf4ec2ca 100644 --- a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs +++ b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs @@ -198,5 +198,17 @@ namespace MediaBrowser.Server.Implementations.Session }, cancellationToken); } + + public Task SendGenericCommand(GenericCommand command, CancellationToken cancellationToken) + { + var socket = GetActiveSocket(); + + return socket.SendAsync(new WebSocketMessage<GenericCommand> + { + MessageType = "Command", + Data = command + + }, cancellationToken); + } } } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index b7e9017d6..2575434f4 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -170,7 +170,7 @@ namespace MediaBrowser.ServerApplication private ILiveTvManager LiveTvManager { get; set; } - private ILocalizationManager LocalizationManager { get; set; } + internal ILocalizationManager LocalizationManager { get; set; } private IEncodingManager EncodingManager { get; set; } private IChannelManager ChannelManager { get; set; } @@ -421,6 +421,9 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(ServerConfigurationManager); + LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer); + RegisterSingleInstance(LocalizationManager); + RegisterSingleInstance<IWebSocketServer>(() => new AlchemyServer(Logger)); RegisterSingleInstance<IBlurayExaminer>(() => new BdInfoExaminer()); @@ -449,6 +452,8 @@ namespace MediaBrowser.ServerApplication LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager); RegisterSingleInstance(LibraryManager); + RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager)); + LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager); RegisterSingleInstance(LibraryMonitor); @@ -470,9 +475,6 @@ namespace MediaBrowser.ServerApplication ServerManager = new ServerManager(this, JsonSerializer, LogManager.GetLogger("ServerManager"), ServerConfigurationManager); RegisterSingleInstance(ServerManager); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager); - RegisterSingleInstance(LocalizationManager); - var innerProgress = new ActionableProgress<double>(); innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); @@ -503,7 +505,7 @@ namespace MediaBrowser.ServerApplication var appThemeManager = new AppThemeManager(ApplicationPaths, FileSystemManager, JsonSerializer, Logger); RegisterSingleInstance<IAppThemeManager>(appThemeManager); - var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("DLNA")); + var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("DLNA"), JsonSerializer); RegisterSingleInstance<IDlnaManager>(dlnaManager); var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor); diff --git a/MediaBrowser.ServerApplication/LibraryViewer.cs b/MediaBrowser.ServerApplication/LibraryViewer.cs index 6c40b549b..8e9091906 100644 --- a/MediaBrowser.ServerApplication/LibraryViewer.cs +++ b/MediaBrowser.ServerApplication/LibraryViewer.cs @@ -16,18 +16,16 @@ namespace MediaBrowser.ServerApplication { private readonly IJsonSerializer _jsonSerializer; private readonly ILibraryManager _libraryManager; - private readonly IDisplayPreferencesRepository _displayPreferencesManager; private readonly IItemRepository _itemRepository; private User _currentUser; - public LibraryViewer(IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IDisplayPreferencesRepository displayPreferencesManager, IItemRepository itemRepo) + public LibraryViewer(IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IItemRepository itemRepo) { InitializeComponent(); _jsonSerializer = jsonSerializer; _libraryManager = libraryManager; - _displayPreferencesManager = displayPreferencesManager; _itemRepository = itemRepo; foreach (var user in userManager.Users) @@ -44,7 +42,7 @@ namespace MediaBrowser.ServerApplication if (e.Node != null) { var item = (BaseItem)e.Node.Tag; - lblType.Text = "Type: " + item.GetType().Name; + lblType.Text = item.GetType().Name; var json = FormatJson(_jsonSerializer.SerializeToString(item)); diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 3d490a1f8..d8bd3938e 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -253,7 +253,7 @@ namespace MediaBrowser.ServerApplication { //Application.EnableVisualStyles(); //Application.SetCompatibleTextRenderingDefault(false); - _serverNotifyIcon = new ServerNotifyIcon(_appHost.LogManager, _appHost, _appHost.ServerConfigurationManager, _appHost.UserManager, _appHost.LibraryManager, _appHost.JsonSerializer, _appHost.DisplayPreferencesRepository, _appHost.ItemRepository); + _serverNotifyIcon = new ServerNotifyIcon(_appHost.LogManager, _appHost, _appHost.ServerConfigurationManager, _appHost.UserManager, _appHost.LibraryManager, _appHost.JsonSerializer, _appHost.DisplayPreferencesRepository, _appHost.ItemRepository, _appHost.LocalizationManager); Application.Run(); } diff --git a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs index 99b6db2d7..27f47787f 100644 --- a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs +++ b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs @@ -4,6 +4,7 @@ using System.Windows.Forms; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -41,6 +42,7 @@ namespace MediaBrowser.ServerApplication private readonly IJsonSerializer _jsonSerializer; private readonly IDisplayPreferencesRepository _displayPreferencesManager; private readonly IItemRepository _itemRepository; + private readonly ILocalizationManager _localization; private LogForm _logForm; public bool Visible @@ -56,10 +58,17 @@ namespace MediaBrowser.ServerApplication } } - public ServerNotifyIcon(ILogManager logManager, IServerApplicationHost appHost, IServerConfigurationManager configurationManager, IUserManager userManager, ILibraryManager libraryManager, IJsonSerializer jsonSerializer, IDisplayPreferencesRepository displayPreferencesManager, IItemRepository itemRepo) + public ServerNotifyIcon(ILogManager logManager, + IServerApplicationHost appHost, + IServerConfigurationManager configurationManager, + IUserManager userManager, ILibraryManager libraryManager, + IJsonSerializer jsonSerializer, + IDisplayPreferencesRepository displayPreferencesManager, + IItemRepository itemRepo, ILocalizationManager localization) { _logger = logManager.GetLogger("MainWindow"); _itemRepository = itemRepo; + _localization = localization; _appHost = appHost; _logManager = logManager; _configurationManager = configurationManager; @@ -118,20 +127,17 @@ namespace MediaBrowser.ServerApplication // cmdExit.Name = "cmdExit"; cmdExit.Size = new System.Drawing.Size(208, 22); - cmdExit.Text = "Exit"; // // cmdCommunity // cmdCommunity.Name = "cmdCommunity"; cmdCommunity.Size = new System.Drawing.Size(208, 22); - cmdCommunity.Text = "Visit Community"; // // cmdLogWindow // cmdLogWindow.CheckOnClick = true; cmdLogWindow.Name = "cmdLogWindow"; cmdLogWindow.Size = new System.Drawing.Size(208, 22); - cmdLogWindow.Text = "Show Log Window"; // // toolStripSeparator1 // @@ -142,13 +148,11 @@ namespace MediaBrowser.ServerApplication // cmdRestart.Name = "cmdRestart"; cmdRestart.Size = new System.Drawing.Size(208, 22); - cmdRestart.Text = "Restart Server"; // // cmdLibraryExplorer // cmdLibraryExplorer.Name = "cmdLibraryExplorer"; cmdLibraryExplorer.Size = new System.Drawing.Size(208, 22); - cmdLibraryExplorer.Text = "Open Library Explorer"; // // toolStripSeparator2 // @@ -159,13 +163,11 @@ namespace MediaBrowser.ServerApplication // cmdConfigure.Name = "cmdConfigure"; cmdConfigure.Size = new System.Drawing.Size(208, 22); - cmdConfigure.Text = "Configure Media Browser"; // // cmdBrowse // cmdBrowse.Name = "cmdBrowse"; cmdBrowse.Size = new System.Drawing.Size(208, 22); - cmdBrowse.Text = "Browse Library"; // // cmdApiDocs // @@ -175,25 +177,21 @@ namespace MediaBrowser.ServerApplication cmdGtihub}); cmdApiDocs.Name = "cmdApiDocs"; cmdApiDocs.Size = new System.Drawing.Size(208, 22); - cmdApiDocs.Text = "View Api Documentation"; // // cmdStandardDocs // cmdStandardDocs.Name = "cmdStandardDocs"; cmdStandardDocs.Size = new System.Drawing.Size(136, 22); - cmdStandardDocs.Text = "Standard"; // // cmdSwagger // cmdSwagger.Name = "cmdSwagger"; cmdSwagger.Size = new System.Drawing.Size(136, 22); - cmdSwagger.Text = "Swagger"; // // cmdGtihub // cmdGtihub.Name = "cmdGtihub"; cmdGtihub.Size = new System.Drawing.Size(136, 22); - cmdGtihub.Text = "Github Wiki"; cmdExit.Click += cmdExit_Click; cmdRestart.Click += cmdRestart_Click; @@ -211,6 +209,8 @@ namespace MediaBrowser.ServerApplication _logManager.LoggerLoaded += LoadLogWindow; _configurationManager.ConfigurationUpdated += Instance_ConfigurationUpdated; + LocalizeText(); + if (_appHost.IsFirstRun) { Action action = () => notifyIcon1.ShowBalloonTip(5000, "Media Browser", "Welcome to Media Browser Server!", ToolTipIcon.Info); @@ -219,6 +219,24 @@ namespace MediaBrowser.ServerApplication } } + private void LocalizeText() + { + _uiCulture = _configurationManager.Configuration.UICulture; + + cmdExit.Text = _localization.GetLocalizedString("LabelExit"); + cmdCommunity.Text = _localization.GetLocalizedString("LabelVisitCommunity"); + cmdGtihub.Text = _localization.GetLocalizedString("LabelGithubWiki"); + cmdSwagger.Text = _localization.GetLocalizedString("LabelSwagger"); + cmdStandardDocs.Text = _localization.GetLocalizedString("LabelStandard"); + cmdApiDocs.Text = _localization.GetLocalizedString("LabelViewApiDocumentation"); + cmdBrowse.Text = _localization.GetLocalizedString("LabelBrowseLibrary"); + cmdConfigure.Text = _localization.GetLocalizedString("LabelConfigureMediaBrowser"); + cmdLibraryExplorer.Text = _localization.GetLocalizedString("LabelOpenLibraryViewer"); + cmdRestart.Text = _localization.GetLocalizedString("LabelRestartServer"); + cmdLogWindow.Text = _localization.GetLocalizedString("LabelShowLogWindow"); + } + + private string _uiCulture; /// <summary> /// Handles the ConfigurationUpdated event of the Instance control. /// </summary> @@ -226,6 +244,12 @@ namespace MediaBrowser.ServerApplication /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> void Instance_ConfigurationUpdated(object sender, EventArgs e) { + if (!string.Equals(_configurationManager.Configuration.UICulture, _uiCulture, + StringComparison.OrdinalIgnoreCase)) + { + LocalizeText(); + } + Action action = () => { var isLogWindowOpen = _logForm != null; @@ -307,7 +331,7 @@ namespace MediaBrowser.ServerApplication void cmdLibraryExplorer_Click(object sender, EventArgs e) { - new LibraryViewer(_jsonSerializer, _userManager, _libraryManager, _displayPreferencesManager, _itemRepository).Show(); + new LibraryViewer(_jsonSerializer, _userManager, _libraryManager, _itemRepository).Show(); } void cmdRestart_Click(object sender, EventArgs e) diff --git a/MediaBrowser.ServerApplication/Splash/SplashForm.Designer.cs b/MediaBrowser.ServerApplication/Splash/SplashForm.Designer.cs index e9ba17bc1..ef3b79699 100644 --- a/MediaBrowser.ServerApplication/Splash/SplashForm.Designer.cs +++ b/MediaBrowser.ServerApplication/Splash/SplashForm.Designer.cs @@ -121,9 +121,9 @@ this.lblStatus.Location = new System.Drawing.Point(3, 0); this.lblStatus.MaximumSize = new System.Drawing.Size(0, 100); this.lblStatus.Name = "lblStatus"; - this.lblStatus.Size = new System.Drawing.Size(599, 59); + this.lblStatus.Size = new System.Drawing.Size(469, 59); this.lblStatus.TabIndex = 0; - this.lblStatus.Text = "Loading Media Browser Server"; + this.lblStatus.Text = "Loading Media Browser"; this.lblStatus.UseWaitCursor = true; // // panel1 diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 99afbbdd7..7313c089f 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -3,9 +3,11 @@ using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; using ServiceStack; using ServiceStack.Web; using System; @@ -15,6 +17,8 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; +using WebMarkupMin.Core.Minifiers; +using WebMarkupMin.Core.Settings; namespace MediaBrowser.WebDashboard.Api { @@ -96,6 +100,8 @@ namespace MediaBrowser.WebDashboard.Api private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + private readonly IJsonSerializer _jsonSerializer; /// <summary> /// Initializes a new instance of the <see cref="DashboardService" /> class. @@ -103,11 +109,13 @@ namespace MediaBrowser.WebDashboard.Api /// <param name="appHost">The app host.</param> /// <param name="serverConfigurationManager">The server configuration manager.</param> /// <param name="fileSystem">The file system.</param> - public DashboardService(IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem) + public DashboardService(IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; + _localization = localization; + _jsonSerializer = jsonSerializer; } /// <summary> @@ -148,7 +156,7 @@ namespace MediaBrowser.WebDashboard.Api { var page = ServerEntryPoint.Instance.PluginConfigurationPages.First(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)); - return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), page.Plugin.AssemblyDateLastModified, null, MimeTypes.GetMimeType("page.html"), () => ModifyHtml(page.GetHtmlStream())); + return ResultFactory.GetStaticResult(Request, page.Plugin.Version.ToString().GetMD5(), page.Plugin.AssemblyDateLastModified, null, MimeTypes.GetMimeType("page.html"), () => ModifyHtml(page.GetHtmlStream(), null)); } /// <summary> @@ -210,13 +218,16 @@ namespace MediaBrowser.WebDashboard.Api var contentType = MimeTypes.GetMimeType(path); + var isHtml = IsHtml(path); + var localizationCulture = isHtml ? GetLocalizationCulture() : null; + // Don't cache if not configured to do so // But always cache images to simulate production - if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && - !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) && + if (!_serverConfigurationManager.Configuration.EnableDashboardResponseCaching && + !contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) && !contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) { - return ResultFactory.GetResult(GetResourceStream(path).Result, contentType); + return ResultFactory.GetResult(GetResourceStream(path, isHtml, localizationCulture).Result, contentType); } TimeSpan? cacheDuration = null; @@ -230,17 +241,24 @@ namespace MediaBrowser.WebDashboard.Api var assembly = GetType().Assembly.GetName(); - var cacheKey = (assembly.Version + path).GetMD5(); + var cacheKey = (assembly.Version + (localizationCulture ?? string.Empty) + path).GetMD5(); - return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path)); + return ResultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, isHtml, localizationCulture)); + } + + private string GetLocalizationCulture() + { + return _serverConfigurationManager.Configuration.UICulture; } /// <summary> /// Gets the resource stream. /// </summary> /// <param name="path">The path.</param> + /// <param name="isHtml">if set to <c>true</c> [is HTML].</param> + /// <param name="localizationCulture">The localization culture.</param> /// <returns>Task{Stream}.</returns> - private async Task<Stream> GetResourceStream(string path) + private async Task<Stream> GetResourceStream(string path, bool isHtml, string localizationCulture) { Stream resourceStream; @@ -259,13 +277,11 @@ namespace MediaBrowser.WebDashboard.Api if (resourceStream != null) { - var isHtml = IsHtml(path); - // Don't apply any caching for html pages // jQuery ajax doesn't seem to handle if-modified-since correctly if (isHtml) { - resourceStream = await ModifyHtml(resourceStream).ConfigureAwait(false); + resourceStream = await ModifyHtml(resourceStream, localizationCulture).ConfigureAwait(false); } } @@ -297,26 +313,48 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <param name="sourceStream">The source stream.</param> /// <returns>Task{Stream}.</returns> - internal async Task<Stream> ModifyHtml(Stream sourceStream) + private async Task<Stream> ModifyHtml(Stream sourceStream, string localizationCulture) { - string html; - - using (var memoryStream = new MemoryStream()) + using (sourceStream) { - await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false); + string html; - html = Encoding.UTF8.GetString(memoryStream.ToArray()); - } + using (var memoryStream = new MemoryStream()) + { + await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false); + + html = Encoding.UTF8.GetString(memoryStream.ToArray()); - var version = GetType().Assembly.GetName().Version; + if (!string.IsNullOrWhiteSpace(localizationCulture)) + { + html = _localization.LocalizeDocument(html, localizationCulture, GetLocalizationToken); + } - html = html.Replace("<head>", "<head>" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version)); + try + { + var minifier = new HtmlMinifier(new HtmlMinificationSettings(true)); - var bytes = Encoding.UTF8.GetBytes(html); + html = minifier.Minify(html).MinifiedContent; + } + catch (Exception ex) + { + Logger.ErrorException("Error minifying html", ex); + } + } + + var version = GetType().Assembly.GetName().Version; + + html = html.Replace("<head>", "<head>" + GetMetaTags() + GetCommonCss(version) + GetCommonJavascript(version)); - sourceStream.Dispose(); + var bytes = Encoding.UTF8.GetBytes(html); - return new MemoryStream(bytes); + return new MemoryStream(bytes); + } + } + + private string GetLocalizationToken(string phrase) + { + return "${" + phrase + "}"; } /// <summary> @@ -393,154 +431,204 @@ namespace MediaBrowser.WebDashboard.Api /// <returns>Task{Stream}.</returns> private async Task<Stream> GetAllJavascript() { - var scriptFiles = new[] - { - "extensions.js", - "site.js", - "librarybrowser.js", - "librarylist.js", - "editorsidebar.js", - "librarymenu.js", - //"chromecast.js", - "contextmenu.js", - - "ratingdialog.js", - "aboutpage.js", - "allusersettings.js", - "alphapicker.js", - "addpluginpage.js", - "advancedconfigurationpage.js", - "advancedpaths.js", - "advancedserversettings.js", - "metadataadvanced.js", - "appsplayback.js", - "appsweather.js", - "autoorganizetv.js", - "autoorganizelog.js", - "channels.js", - "channelitems.js", - "dashboardinfo.js", - "dashboardpage.js", - "directorybrowser.js", - "dlnaprofile.js", - "dlnaprofiles.js", - "dlnasettings.js", - "editcollectionitems.js", - "edititemmetadata.js", - "edititempeople.js", - "edititemimages.js", - "encodingsettings.js", - "gamesrecommendedpage.js", - "gamesystemspage.js", - "gamespage.js", - "gamegenrepage.js", - "gamestudiospage.js", - "indexpage.js", - "itembynamedetailpage.js", - "itemdetailpage.js", - "itemgallery.js", - "itemlistpage.js", - "librarypathmapping.js", - "libraryreport.js", - "librarysettings.js", - "livetvchannel.js", - "livetvchannels.js", - "livetvguide.js", - "livetvnewrecording.js", - "livetvprogram.js", - "livetvrecording.js", - "livetvrecordinglist.js", - "livetvrecordings.js", - "livetvtimer.js", - "livetvseriestimer.js", - "livetvseriestimers.js", - "livetvsettings.js", - "livetvsuggested.js", - "livetvstatus.js", - "livetvtimers.js", - "loginpage.js", - "logpage.js", - "medialibrarypage.js", - "mediaplayer.js", - - "mediaplayer-video.js", - - "metadataconfigurationpage.js", - "metadataimagespage.js", - "moviegenres.js", - "moviecollections.js", - "movies.js", - "movieslatest.js", - "moviepeople.js", - "moviesrecommended.js", - "moviestudios.js", - "movietrailers.js", - "musicalbums.js", - "musicalbumartists.js", - "musicartists.js", - "musicgenres.js", - "musicrecommended.js", - "musicvideos.js", - "notifications.js", - "playlist.js", - "plugincatalogpage.js", - "pluginspage.js", - "pluginupdatespage.js", - "remotecontrol.js", - "scheduledtaskpage.js", - "scheduledtaskspage.js", - "search.js", - "songs.js", - "supporterkeypage.js", - "supporterpage.js", - "episodes.js", - "tvgenres.js", - "tvlatest.js", - "tvpeople.js", - "tvrecommended.js", - "tvshows.js", - "tvstudios.js", - "tvupcoming.js", - "useredit.js", - "userpassword.js", - "userimagepage.js", - "userprofilespage.js", - "usersettings.js", - "userparentalcontrol.js", - "wizardfinishpage.js", - "wizardimagesettings.js", - "wizardservice.js", - "wizardstartpage.js", - "wizardsettings.js", - "wizarduserpage.js" - }; - var memoryStream = new MemoryStream(); var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine); + // jQuery + jQuery mobile await AppendResource(memoryStream, "thirdparty/jquery-2.0.3.min.js", newLineBytes).ConfigureAwait(false); await AppendResource(memoryStream, "thirdparty/jquerymobile-1.4.2/jquery.mobile-1.4.2.min.js", newLineBytes).ConfigureAwait(false); + await AppendLocalization(memoryStream).ConfigureAwait(false); + await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); + + // Write the version string for the dashboard comparison function var versionString = string.Format("window.dashboardVersion='{0}';", _appHost.ApplicationVersion); var versionBytes = Encoding.UTF8.GetBytes(versionString); await memoryStream.WriteAsync(versionBytes, 0, versionBytes.Length).ConfigureAwait(false); await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); - await AppendResource(memoryStream, "thirdparty/autonumeric/autoNumeric.min.js", newLineBytes).ConfigureAwait(false); - + var builder = new StringBuilder(); var assembly = GetType().Assembly; - await AppendResource(assembly, memoryStream, "MediaBrowser.WebDashboard.ApiClient.js", newLineBytes).ConfigureAwait(false); + using (var stream = assembly.GetManifestResourceStream("MediaBrowser.WebDashboard.ApiClient.js")) + { + using (var streamReader = new StreamReader(stream)) + { + var text = await streamReader.ReadToEndAsync().ConfigureAwait(false); + builder.Append(text); + builder.Append(Environment.NewLine); + } + } + + foreach (var file in GetScriptFiles()) + { + var path = GetDashboardResourcePath("scripts/" + file); + + using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + { + using (var streamReader = new StreamReader(fs)) + { + var text = await streamReader.ReadToEndAsync().ConfigureAwait(false); + builder.Append(text); + builder.Append(Environment.NewLine); + } + } + } + + var js = builder.ToString(); + + try + { + var result = new CrockfordJsMinifier().Minify(js, false, Encoding.UTF8); - foreach (var file in scriptFiles) + js = result.MinifiedContent; + } + catch (Exception ex) { - await AppendResource(memoryStream, "scripts/" + file, newLineBytes).ConfigureAwait(false); + Logger.ErrorException("Error minifying javascript", ex); } + var bytes = Encoding.UTF8.GetBytes(js); + await memoryStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + memoryStream.Position = 0; return memoryStream; } + private IEnumerable<string> GetScriptFiles() + { + return new[] + { + "extensions.js", + "site.js", + "librarybrowser.js", + "librarylist.js", + "editorsidebar.js", + "librarymenu.js", + "chromecast.js", + "contextmenu.js", + + "mediacontroller.js", + "mediaplayer.js", + "mediaplayer-video.js", + + "ratingdialog.js", + "aboutpage.js", + "allusersettings.js", + "alphapicker.js", + "addpluginpage.js", + "advancedconfigurationpage.js", + "advancedpaths.js", + "advancedserversettings.js", + "metadataadvanced.js", + "appsplayback.js", + "appsweather.js", + "autoorganizetv.js", + "autoorganizelog.js", + "channels.js", + "channelitems.js", + "dashboardgeneral.js", + "dashboardinfo.js", + "dashboardpage.js", + "directorybrowser.js", + "dlnaprofile.js", + "dlnaprofiles.js", + "dlnasettings.js", + "editcollectionitems.js", + "edititemmetadata.js", + "edititempeople.js", + "edititemimages.js", + "encodingsettings.js", + "gamesrecommendedpage.js", + "gamesystemspage.js", + "gamespage.js", + "gamegenrepage.js", + "gamestudiospage.js", + "indexpage.js", + "itembynamedetailpage.js", + "itemdetailpage.js", + "itemgallery.js", + "itemlistpage.js", + "librarypathmapping.js", + "libraryreport.js", + "librarysettings.js", + "livetvchannel.js", + "livetvchannels.js", + "livetvguide.js", + "livetvnewrecording.js", + "livetvprogram.js", + "livetvrecording.js", + "livetvrecordinglist.js", + "livetvrecordings.js", + "livetvtimer.js", + "livetvseriestimer.js", + "livetvseriestimers.js", + "livetvsettings.js", + "livetvsuggested.js", + "livetvstatus.js", + "livetvtimers.js", + "loginpage.js", + "logpage.js", + "medialibrarypage.js", + "metadataconfigurationpage.js", + "metadataimagespage.js", + "moviegenres.js", + "moviecollections.js", + "movies.js", + "movieslatest.js", + "moviepeople.js", + "moviesrecommended.js", + "moviestudios.js", + "movietrailers.js", + "musicalbums.js", + "musicalbumartists.js", + "musicartists.js", + "musicgenres.js", + "musicrecommended.js", + "musicvideos.js", + "notifications.js", + "playlist.js", + "plugincatalogpage.js", + "pluginspage.js", + "pluginupdatespage.js", + "remotecontrol.js", + "scheduledtaskpage.js", + "scheduledtaskspage.js", + "search.js", + "songs.js", + "supporterkeypage.js", + "supporterpage.js", + "episodes.js", + "tvgenres.js", + "tvlatest.js", + "tvpeople.js", + "tvrecommended.js", + "tvshows.js", + "tvstudios.js", + "tvupcoming.js", + "useredit.js", + "userpassword.js", + "userimagepage.js", + "userprofilespage.js", + "usersettings.js", + "userparentalcontrol.js", + "wizardfinishpage.js", + "wizardimagesettings.js", + "wizardservice.js", + "wizardstartpage.js", + "wizardsettings.js", + "wizarduserpage.js" + }; + } + + private async Task AppendLocalization(Stream stream) + { + var js = "window.localizationGlossary=" + _jsonSerializer.SerializeToString(_localization.GetJavaScriptLocalizationDictionary(GetLocalizationCulture())); + + var bytes = Encoding.UTF8.GetBytes(js); + await stream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + } + /// <summary> /// Gets all CSS. /// </summary> @@ -568,35 +656,40 @@ namespace MediaBrowser.WebDashboard.Api "icons.css" }; - var memoryStream = new MemoryStream(); - - var newLineBytes = Encoding.UTF8.GetBytes(Environment.NewLine); + var builder = new StringBuilder(); foreach (var file in files) { - await AppendResource(memoryStream, "css/" + file, newLineBytes).ConfigureAwait(false); + var path = GetDashboardResourcePath("css/" + file); + + using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + { + using (var streamReader = new StreamReader(fs)) + { + var text = await streamReader.ReadToEndAsync().ConfigureAwait(false); + builder.Append(text); + builder.Append(Environment.NewLine); + } + } } - - memoryStream.Position = 0; - return memoryStream; - } - /// <summary> - /// Appends the resource. - /// </summary> - /// <param name="assembly">The assembly.</param> - /// <param name="outputStream">The output stream.</param> - /// <param name="path">The path.</param> - /// <param name="newLineBytes">The new line bytes.</param> - /// <returns>Task.</returns> - private async Task AppendResource(Assembly assembly, Stream outputStream, string path, byte[] newLineBytes) - { - using (var stream = assembly.GetManifestResourceStream(path)) - { - await stream.CopyToAsync(outputStream).ConfigureAwait(false); + var css = builder.ToString(); - await outputStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false); - } + //try + //{ + // var result = new KristensenCssMinifier().Minify(builder.ToString(), false, Encoding.UTF8); + + // css = result.MinifiedContent; + //} + //catch (Exception ex) + //{ + // Logger.ErrorException("Error minifying css", ex); + //} + + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(css)); + + memoryStream.Position = 0; + return memoryStream; } /// <summary> diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index ea1905bad..f268c44ec 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -4058,7 +4058,7 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi options.ForceTitle = true; } - var url = self.getUrl("Packages/" + packageId + "Reviews", options); + var url = self.getUrl("Packages/" + packageId + "/Reviews", options); return self.ajax({ type: "GET", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 6a8cc49b1..0ddb05577 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -57,6 +57,9 @@ <Reference Include="ServiceStack.Interfaces">
<HintPath>..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll</HintPath>
</Reference>
+ <Reference Include="WebMarkupMin.Core">
+ <HintPath>..\packages\WebMarkupMin.Core.0.8.18\lib\net40\WebMarkupMin.Core.dll</HintPath>
+ </Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs">
@@ -221,6 +224,9 @@ <Content Include="dashboard-ui\css\mediaplayer.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\dashboardgeneral.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\dashboardinfopage.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -515,6 +521,9 @@ <Content Include="dashboard-ui\scripts\contextmenu.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\dashboardgeneral.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\dashboardinfo.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -587,6 +596,9 @@ <Content Include="dashboard-ui\scripts\editorsidebar.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\mediacontroller.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\mediaplayer-video.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -638,9 +650,6 @@ <Content Include="dashboard-ui\livetvseriestimers.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\autonumeric\autoNumeric.min.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\thirdparty\jquery-2.0.3.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -1474,9 +1483,6 @@ <Content Include="dashboard-ui\scripts\tvstudios.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\autonumeric\autoNumeric.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\thirdparty\jstree1.0\jquery.jstree.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -1967,6 +1973,7 @@ </Content>
</ItemGroup>
<ItemGroup>
+ <None Include="app.config" />
<None Include="dashboard-ui\css\fonts\OpenSans-ExtraBold.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@@ -1980,6 +1987,9 @@ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
+ <None Include="WebMarkupMin.Configuration.xsd">
+ <SubType>Designer</SubType>
+ </None>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
diff --git a/MediaBrowser.WebDashboard/app.config b/MediaBrowser.WebDashboard/app.config new file mode 100644 index 000000000..8c2c664cd --- /dev/null +++ b/MediaBrowser.WebDashboard/app.config @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <configSections> + <sectionGroup name="webMarkupMin"> + <section name="core" type="WebMarkupMin.Core.Configuration.CoreConfiguration, WebMarkupMin.Core" /> + </sectionGroup> + </configSections> + <webMarkupMin xmlns="http://tempuri.org/WebMarkupMin.Configuration.xsd"> + <core> + <css> + <minifiers> + <add name="NullCssMinifier" displayName="Null CSS Minifier" type="WebMarkupMin.Core.Minifiers.NullCssMinifier, WebMarkupMin.Core" /> + <add name="KristensenCssMinifier" displayName="Mads Kristensen's CSS minifier" type="WebMarkupMin.Core.Minifiers.KristensenCssMinifier, WebMarkupMin.Core" /> + </minifiers> + </css> + <js> + <minifiers> + <add name="NullJsMinifier" displayName="Null JS Minifier" type="WebMarkupMin.Core.Minifiers.NullJsMinifier, WebMarkupMin.Core" /> + <add name="CrockfordJsMinifier" displayName="Douglas Crockford's JS Minifier" type="WebMarkupMin.Core.Minifiers.CrockfordJsMinifier, WebMarkupMin.Core" /> + </minifiers> + </js> + <logging> + <loggers> + <add name="NullLogger" displayName="Null Logger" type="WebMarkupMin.Core.Loggers.NullLogger, WebMarkupMin.Core" /> + <add name="ThrowExceptionLogger" displayName="Throw exception logger" type="WebMarkupMin.Core.Loggers.ThrowExceptionLogger, WebMarkupMin.Core" /> + </loggers> + </logging> + </core> + </webMarkupMin> +</configuration>
\ No newline at end of file diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index a5074d3c8..b867f7a92 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <package id="MediaBrowser.ApiClient.Javascript" version="3.0.248" targetFramework="net45" /> + <package id="MediaBrowser.ApiClient.Javascript" version="3.0.249" targetFramework="net45" /> + <package id="WebMarkupMin.Core" version="0.8.18" targetFramework="net45" /> </packages>
\ No newline at end of file diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index c550b350c..a3b4533dd 100644 --- a/Nuget/MediaBrowser.Common.Internal.nuspec +++ b/Nuget/MediaBrowser.Common.Internal.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common.Internal</id> - <version>3.0.345</version> + <version>3.0.346</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.345" /> + <dependency id="MediaBrowser.Common" version="3.0.346" /> <dependency id="NLog" version="2.1.0" /> <dependency id="SimpleInjector" version="2.4.1" /> <dependency id="sharpcompress" version="0.10.2" /> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index a6fa5c152..b80f673e3 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Common</id> - <version>3.0.345</version> + <version>3.0.346</version> <title>MediaBrowser.Common</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index cc87b0030..eccfcccd2 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>MediaBrowser.Server.Core</id> - <version>3.0.345</version> + <version>3.0.346</version> <title>Media Browser.Server.Core</title> <authors>Media Browser Team</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains core components required to build plugins for Media Browser Server.</description> <copyright>Copyright © Media Browser 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.345" /> + <dependency id="MediaBrowser.Common" version="3.0.346" /> </dependencies> </metadata> <files> |
