diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-02-14 16:35:09 -0500 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2016-02-14 16:35:09 -0500 |
| commit | daa0b6cd0ecefd60611752802d062c15e6da85de (patch) | |
| tree | f58a16e47afed9b61471e3871280faa9fc8bd951 | |
| parent | 7d26b8995f313917829573a7cd96c37decc9158a (diff) | |
| parent | fd5f12e76227d96c52cdc31b67ef9543b485169b (diff) | |
Merge branch 'beta'
229 files changed, 4627 insertions, 2545 deletions
diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index c9b1c5c53..c3b031448 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -183,50 +183,6 @@ namespace MediaBrowser.Api return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager)); } - protected IList<BaseItem> GetAllLibraryItems(string userId, IUserManager userManager, ILibraryManager libraryManager, string parentId, Func<BaseItem,bool> filter) - { - if (!string.IsNullOrEmpty(parentId)) - { - var folder = (Folder)libraryManager.GetItemById(new Guid(parentId)); - - if (!string.IsNullOrWhiteSpace(userId)) - { - var user = userManager.GetUserById(userId); - - if (user == null) - { - throw new ArgumentException("User not found"); - } - - return folder - .GetRecursiveChildren(user, filter) - .ToList(); - } - - return folder - .GetRecursiveChildren(filter); - } - if (!string.IsNullOrWhiteSpace(userId)) - { - var user = userManager.GetUserById(userId); - - if (user == null) - { - throw new ArgumentException("User not found"); - } - - return userManager - .GetUserById(userId) - .RootFolder - .GetRecursiveChildren(user, filter) - .ToList(); - } - - return libraryManager - .RootFolder - .GetRecursiveChildren(filter); - } - /// <summary> /// Deslugs an artist name by finding the correct entry in the library /// </summary> diff --git a/MediaBrowser.Api/GamesService.cs b/MediaBrowser.Api/GamesService.cs index 93cc01079..a27c872f1 100644 --- a/MediaBrowser.Api/GamesService.cs +++ b/MediaBrowser.Api/GamesService.cs @@ -102,12 +102,16 @@ namespace MediaBrowser.Api /// <returns>System.Object.</returns> public object Get(GetGameSystemSummaries request) { - var gameSystems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, i => i is GameSystem) + var user = request.UserId == null ? null : _userManager.GetUserById(request.UserId); + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(GameSystem).Name } + }; + var parentIds = new string[] { } ; + var gameSystems = _libraryManager.GetItems(query, parentIds) .Cast<GameSystem>() .ToList(); - var user = request.UserId == null ? null : _userManager.GetUserById(request.UserId); - var result = gameSystems .Select(i => GetSummary(i, user)) .ToList(); @@ -119,8 +123,15 @@ namespace MediaBrowser.Api public object Get(GetPlayerIndex request) { - var games = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, null, i => i is Game) - .Cast<Game>(); + var user = request.UserId == null ? null : _userManager.GetUserById(request.UserId); + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Game).Name } + }; + var parentIds = new string[] { }; + var games = _libraryManager.GetItems(query, parentIds) + .Cast<Game>() + .ToList(); var lookup = games .ToLookup(i => i.PlayersSupported ?? -1) diff --git a/MediaBrowser.Api/Library/FileOrganizationService.cs b/MediaBrowser.Api/Library/FileOrganizationService.cs index 29a982629..1224fa957 100644 --- a/MediaBrowser.Api/Library/FileOrganizationService.cs +++ b/MediaBrowser.Api/Library/FileOrganizationService.cs @@ -1,9 +1,11 @@ -using MediaBrowser.Controller.FileOrganization; +using System.Collections.Generic; +using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Net; using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.Querying; using ServiceStack; using System.Threading.Tasks; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Api.Library { @@ -74,6 +76,31 @@ namespace MediaBrowser.Api.Library public bool RememberCorrection { get; set; } } + [Route("/Library/FileOrganizations/SmartMatches", "GET", Summary = "Gets smart match entries")] + public class GetSmartMatchInfos : IReturn<QueryResult<SmartMatchInfo>> + { + /// <summary> + /// Skips over a given number of items within the results. Use for paging. + /// </summary> + /// <value>The start index.</value> + [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? StartIndex { get; set; } + + /// <summary> + /// The maximum number of items to return + /// </summary> + /// <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; } + } + + [Route("/Library/FileOrganizations/SmartMatches/Delete", "POST", Summary = "Deletes a smart match entry")] + public class DeleteSmartMatchEntry + { + [ApiMember(Name = "Entries", Description = "SmartMatch Entry", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public List<NameValuePair> Entries { get; set; } + } + [Authenticated(Roles = "Admin")] public class FileOrganizationService : BaseApiService { @@ -130,5 +157,24 @@ namespace MediaBrowser.Api.Library Task.WaitAll(task); } + + public object Get(GetSmartMatchInfos request) + { + var result = _iFileOrganizationService.GetSmartMatchInfos(new FileOrganizationResultQuery + { + Limit = request.Limit, + StartIndex = request.StartIndex + }); + + return ToOptimizedSerializedResultUsingCache(result); + } + + public void Post(DeleteSmartMatchEntry request) + { + request.Entries.ForEach(entry => + { + _iFileOrganizationService.DeleteSmartMatchEntry(entry.Name, entry.Value); + }); + } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index b7066a36d..896f8c990 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -424,7 +424,7 @@ namespace MediaBrowser.Api.Library public object Get(GetMediaFolders request) { - var items = _libraryManager.GetUserRootFolder().Children.OrderBy(i => i.SortName).ToList(); + var items = _libraryManager.GetUserRootFolder().Children.Concat(_libraryManager.RootFolder.VirtualChildren).OrderBy(i => i.SortName).ToList(); if (request.IsHidden.HasValue) { @@ -569,7 +569,7 @@ namespace MediaBrowser.Api.Library { throw new ArgumentException("This command cannot be used for remote or virtual items."); } - if (_fileSystem.DirectoryExists(item.Path)) + if (_fileSystem.DirectoryExists(item.Path)) { throw new ArgumentException("This command cannot be used for directories."); } @@ -618,7 +618,7 @@ namespace MediaBrowser.Api.Library var dtoOptions = GetDtoOptions(request); - BaseItem parent = item.Parent; + BaseItem parent = item.GetParent(); while (parent != null) { @@ -629,7 +629,7 @@ namespace MediaBrowser.Api.Library baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); - parent = parent.Parent; + parent = parent.GetParent(); } return baseItemDtos.ToList(); @@ -637,7 +637,7 @@ namespace MediaBrowser.Api.Library private BaseItem TranslateParentItem(BaseItem item, User user) { - if (item.Parent is AggregateFolder) + if (item.GetParent() is AggregateFolder) { return user.RootFolder.GetChildren(user, true).FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)); } @@ -685,6 +685,50 @@ namespace MediaBrowser.Api.Library return ToOptimizedSerializedResultUsingCache(counts); } + private IList<BaseItem> GetAllLibraryItems(string userId, IUserManager userManager, ILibraryManager libraryManager, string parentId, Func<BaseItem, bool> filter) + { + if (!string.IsNullOrEmpty(parentId)) + { + var folder = (Folder)libraryManager.GetItemById(new Guid(parentId)); + + if (!string.IsNullOrWhiteSpace(userId)) + { + var user = userManager.GetUserById(userId); + + if (user == null) + { + throw new ArgumentException("User not found"); + } + + return folder + .GetRecursiveChildren(user, filter) + .ToList(); + } + + return folder + .GetRecursiveChildren(filter); + } + if (!string.IsNullOrWhiteSpace(userId)) + { + var user = userManager.GetUserById(userId); + + if (user == null) + { + throw new ArgumentException("User not found"); + } + + return userManager + .GetUserById(userId) + .RootFolder + .GetRecursiveChildren(user, filter) + .ToList(); + } + + return libraryManager + .RootFolder + .GetRecursiveChildren(filter); + } + private bool FilterItem(BaseItem item, GetItemCounts request, string userId) { if (!string.IsNullOrWhiteSpace(userId)) @@ -745,12 +789,10 @@ namespace MediaBrowser.Api.Library return Task.FromResult(true); } - if (item is ILiveTvRecording) + return item.Delete(new DeleteOptions { - return _liveTv.DeleteRecording(i); - } - - return _libraryManager.DeleteItem(item); + DeleteFileLocation = true + }); }).ToArray(); Task.WaitAll(tasks); @@ -847,9 +889,9 @@ namespace MediaBrowser.Api.Library : (Folder)_libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); - while (GetThemeSongIds(item).Count == 0 && request.InheritFromParent && item.Parent != null) + while (GetThemeSongIds(item).Count == 0 && request.InheritFromParent && item.GetParent() != null) { - item = item.Parent; + item = item.GetParent(); } var dtoOptions = GetDtoOptions(request); @@ -890,9 +932,9 @@ namespace MediaBrowser.Api.Library : (Folder)_libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); - while (GetThemeVideoIds(item).Count == 0 && request.InheritFromParent && item.Parent != null) + while (GetThemeVideoIds(item).Count == 0 && request.InheritFromParent && item.GetParent() != null) { - item = item.Parent; + item = item.GetParent(); } var dtoOptions = GetDtoOptions(request); diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 7e55446ae..f711c69e6 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -80,6 +80,7 @@ <Compile Include="FilterService.cs" /> <Compile Include="IHasDtoOptions.cs" /> <Compile Include="Library\ChapterService.cs" /> + <Compile Include="PinLoginService.cs" /> <Compile Include="Playback\Dash\ManifestBuilder.cs" /> <Compile Include="Playback\Dash\MpegDashService.cs" /> <Compile Include="Playback\MediaInfoService.cs" /> diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index fe8bae1a5..36cbc6ffa 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -117,10 +117,7 @@ namespace MediaBrowser.Api.Movies public async Task<object> Get(GetSimilarMovies request) { var result = await GetSimilarItemsResult( - // Strip out secondary versions - request, item => (item is Movie) && !((Video)item).PrimaryVersionId.HasValue, - - SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); + request, SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } @@ -128,10 +125,7 @@ namespace MediaBrowser.Api.Movies public async Task<object> Get(GetSimilarTrailers request) { var result = await GetSimilarItemsResult( - // Strip out secondary versions - request, item => (item is Movie) && !((Video)item).PrimaryVersionId.HasValue, - - SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); + request, SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } @@ -140,8 +134,12 @@ namespace MediaBrowser.Api.Movies { var user = _userManager.GetUserById(request.UserId); - IEnumerable<BaseItem> movies = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, i => i is Movie); - + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Movie).Name } + }; + var parentIds = string.IsNullOrWhiteSpace(request.ParentId) ? new string[] { } : new[] { request.ParentId }; + var movies = _libraryManager.GetItems(query, parentIds); movies = _libraryManager.ReplaceVideosWithPrimaryVersions(movies); var listEligibleForCategories = new List<BaseItem>(); @@ -184,21 +182,27 @@ namespace MediaBrowser.Api.Movies return ToOptimizedResult(result); } - private async Task<ItemsResult> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, Func<BaseItem, bool> includeInSearch, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore) + private async Task<ItemsResult> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore) { var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; var item = string.IsNullOrEmpty(request.Id) ? (!string.IsNullOrWhiteSpace(request.UserId) ? user.RootFolder : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); - - Func<BaseItem, bool> filter = i => i.Id != item.Id && includeInSearch(i); - - var inputItems = user == null - ? _libraryManager.RootFolder.GetRecursiveChildren(filter) - : user.RootFolder.GetRecursiveChildren(user, filter); - - var list = inputItems.ToList(); + + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Movie).Name } + }; + var parentIds = new string[] { }; + var list = _libraryManager.GetItems(query, parentIds) + .Where(i => + { + // Strip out secondary versions + var v = i as Video; + return v != null && !v.PrimaryVersionId.HasValue; + }) + .ToList(); if (user != null && user.Configuration.IncludeTrailersInSuggestions) { @@ -379,9 +383,10 @@ namespace MediaBrowser.Api.Movies { foreach (var name in names) { - var itemsWithActor = _libraryManager.GetItemIds(new InternalItemsQuery + var itemsWithActor = _libraryManager.GetItemIds(new InternalItemsQuery(user) { Person = name + }); var items = allMovies diff --git a/MediaBrowser.Api/PackageReviewService.cs b/MediaBrowser.Api/PackageReviewService.cs index b4656b83e..e70d6a713 100644 --- a/MediaBrowser.Api/PackageReviewService.cs +++ b/MediaBrowser.Api/PackageReviewService.cs @@ -102,7 +102,7 @@ namespace MediaBrowser.Api { private readonly IHttpClient _httpClient; private readonly IJsonSerializer _serializer; - private const string MbAdminUrl = "http://www.mb3admin.com/admin/"; + private const string MbAdminUrl = "https://www.mb3admin.com/admin/"; private readonly IServerApplicationHost _appHost; public PackageReviewService(IHttpClient httpClient, IJsonSerializer serializer, IServerApplicationHost appHost) diff --git a/MediaBrowser.Api/PinLoginService.cs b/MediaBrowser.Api/PinLoginService.cs new file mode 100644 index 000000000..8b63de10a --- /dev/null +++ b/MediaBrowser.Api/PinLoginService.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Connect; +using ServiceStack; + +namespace MediaBrowser.Api +{ + [Route("/Auth/Pin", "POST", Summary = "Creates a pin request")] + public class CreatePinRequest : IReturn<PinCreationResult> + { + [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string DeviceId { get; set; } + } + + [Route("/Auth/Pin", "GET", Summary = "Gets pin status")] + public class GetPinStatusRequest : IReturn<PinStatusResult> + { + [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string DeviceId { get; set; } + [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string Pin { get; set; } + } + + [Route("/Auth/Pin/Exchange", "POST", Summary = "Exchanges a pin")] + public class ExchangePinRequest : IReturn<PinExchangeResult> + { + [ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string DeviceId { get; set; } + [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Pin { get; set; } + } + + [Route("/Auth/Pin/Validate", "POST", Summary = "Validates a pin")] + [Authenticated] + public class ValidatePinRequest : IReturnVoid + { + [ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string Pin { get; set; } + } + + public class PinLoginService : BaseApiService + { + private readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase); + + public object Post(CreatePinRequest request) + { + var pin = GetNewPin(); + + var value = new MyPinStatus + { + CreationTimeUtc = DateTime.UtcNow, + IsConfirmed = false, + IsExpired = false, + Pin = pin, + DeviceId = request.DeviceId + }; + + _activeRequests.AddOrUpdate(pin, value, (k, v) => value); + + return ToOptimizedResult(new PinCreationResult + { + DeviceId = request.DeviceId, + IsConfirmed = false, + IsExpired = false, + Pin = pin + }); + } + + public object Get(GetPinStatusRequest request) + { + MyPinStatus status; + + if (!_activeRequests.TryGetValue(request.Pin, out status)) + { + throw new ResourceNotFoundException(); + } + + EnsureValid(request.DeviceId, status); + + return ToOptimizedResult(new PinStatusResult + { + Pin = status.Pin, + IsConfirmed = status.IsConfirmed, + IsExpired = status.IsExpired + }); + } + + public object Post(ExchangePinRequest request) + { + MyPinStatus status; + + if (!_activeRequests.TryGetValue(request.Pin, out status)) + { + throw new ResourceNotFoundException(); + } + + EnsureValid(request.DeviceId, status); + + if (!status.IsConfirmed) + { + throw new ResourceNotFoundException(); + } + + return ToOptimizedResult(new PinExchangeResult + { + // TODO: Add access token + UserId = status.UserId + }); + } + + public void Post(ValidatePinRequest request) + { + MyPinStatus status; + + if (!_activeRequests.TryGetValue(request.Pin, out status)) + { + throw new ResourceNotFoundException(); + } + + EnsureValid(status); + + status.IsConfirmed = true; + status.UserId = AuthorizationContext.GetAuthorizationInfo(Request).UserId; + } + + private void EnsureValid(string requestedDeviceId, MyPinStatus status) + { + if (!string.Equals(requestedDeviceId, status.DeviceId, StringComparison.OrdinalIgnoreCase)) + { + throw new ResourceNotFoundException(); + } + + EnsureValid(status); + } + + private void EnsureValid(MyPinStatus status) + { + if ((DateTime.UtcNow - status.CreationTimeUtc).TotalMinutes > 10) + { + status.IsExpired = true; + } + + if (status.IsExpired) + { + throw new ResourceNotFoundException(); + } + } + + private string GetNewPin() + { + var pin = GetNewPinInternal(); + + while (IsPinActive(pin)) + { + pin = GetNewPinInternal(); + } + + return pin; + } + + private string GetNewPinInternal() + { + var length = 5; + var pin = string.Empty; + + while (pin.Length < length) + { + var digit = new Random().Next(0, 9); + pin += digit.ToString(CultureInfo.InvariantCulture); + } + + return pin; + } + + private bool IsPinActive(string pin) + { + MyPinStatus status; + + if (!_activeRequests.TryGetValue(pin, out status)) + { + return true; + } + + if (status.IsExpired) + { + return true; + } + + return false; + } + + public class MyPinStatus : PinStatusResult + { + public DateTime CreationTimeUtc { get; set; } + public string DeviceId { get; set; } + public string UserId { get; set; } + } + } +} diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index bb7f6bb1e..bae8074fd 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -305,9 +305,8 @@ namespace MediaBrowser.Api.Playback /// </summary> /// <param name="state">The state.</param> /// <param name="videoCodec">The video codec.</param> - /// <param name="isHls">if set to <c>true</c> [is HLS].</param> /// <returns>System.String.</returns> - protected string GetVideoQualityParam(StreamState state, string videoCodec, bool isHls) + protected string GetVideoQualityParam(StreamState state, string videoCodec) { var param = string.Empty; @@ -385,7 +384,7 @@ namespace MediaBrowser.Api.Playback param = "-mbd 2"; } - param += GetVideoBitrateParam(state, videoCodec, isHls); + param += GetVideoBitrateParam(state, videoCodec); var framerate = GetFramerateParam(state); if (framerate.HasValue) @@ -1190,7 +1189,7 @@ namespace MediaBrowser.Api.Playback return bitrate; } - protected string GetVideoBitrateParam(StreamState state, string videoCodec, bool isHls) + protected string GetVideoBitrateParam(StreamState state, string videoCodec) { var bitrate = state.OutputVideoBitrate; @@ -1209,14 +1208,9 @@ namespace MediaBrowser.Api.Playback } // h264 - if (isHls) - { - return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", - bitrate.Value.ToString(UsCulture), - (bitrate.Value * 2).ToString(UsCulture)); - } - - return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); + return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", + bitrate.Value.ToString(UsCulture), + (bitrate.Value * 2).ToString(UsCulture)); } return string.Empty; diff --git a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs index 9ec16fcc7..defb2eef0 100644 --- a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs +++ b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs @@ -430,7 +430,7 @@ namespace MediaBrowser.Api.Playback.Dash var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - args += " " + GetVideoQualityParam(state, GetH264Encoder(state), true) + keyFrameArg; + args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg; // Add resolution params, if specified if (!hasGraphicalSubs) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index baf662c34..4a83615b4 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -822,7 +822,7 @@ namespace MediaBrowser.Api.Playback.Hls var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - args += " " + GetVideoQualityParam(state, GetH264Encoder(state), true) + keyFrameArg; + args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg; //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index be1db1a4d..22c38009a 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -106,7 +106,7 @@ namespace MediaBrowser.Api.Playback.Hls var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - args += " " + GetVideoQualityParam(state, GetH264Encoder(state), true) + keyFrameArg; + args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg; // Add resolution params, if specified if (!hasGraphicalSubs) diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 19e568ec4..eaf65bd6b 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -166,7 +166,7 @@ namespace MediaBrowser.Api.Playback.Progressive args += GetOutputSizeParam(state, videoCodec); } - var qualityParam = GetVideoQualityParam(state, videoCodec, false); + var qualityParam = GetVideoQualityParam(state, videoCodec); if (!string.IsNullOrEmpty(qualityParam)) { diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 602119d4f..89fc29f3d 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -284,7 +284,7 @@ namespace MediaBrowser.Api private T GetParentWithImage<T>(BaseItem item, ImageType type) where T : BaseItem { - return item.Parents.OfType<T>().FirstOrDefault(i => i.HasImage(type)); + return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type)); } } } diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index b05a1bd20..06db1de74 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -66,12 +66,8 @@ namespace MediaBrowser.Api { _config.Configuration.IsStartupWizardCompleted = true; _config.Configuration.EnableLocalizedGuids = true; - _config.Configuration.EnableLibraryMetadataSubFolder = true; _config.Configuration.EnableCustomPathSubFolders = true; - _config.Configuration.DisableStartupScan = true; - _config.Configuration.EnableUserViews = true; _config.Configuration.EnableDateLastRefresh = true; - _config.Configuration.MergeMetadataAndImagesByName = true; _config.SaveConfiguration(); } diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 29a4a8bb5..2dad9533f 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -159,7 +159,7 @@ namespace MediaBrowser.Api [ApiMember(Name = "StartItemId", Description = "Optional. Skip through the list until a given item is found.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string StartItemId { get; set; } - + /// <summary> /// Skips over a given number of items within the results. Use for paging. /// </summary> @@ -273,29 +273,28 @@ namespace MediaBrowser.Api { var user = _userManager.GetUserById(request.UserId); - var items = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, i => i is Episode); - - var itemsList = _libraryManager - .Sort(items, user, new[] { "PremiereDate", "AirTime", "SortName" }, SortOrder.Ascending) - .Cast<Episode>() - .ToList(); - - var unairedEpisodes = itemsList.Where(i => i.IsUnaired).ToList(); - var minPremiereDate = DateTime.Now.Date.AddDays(-1).ToUniversalTime(); - var previousEpisodes = itemsList.Where(i => !i.IsUnaired && (i.PremiereDate ?? DateTime.MinValue) >= minPremiereDate).ToList(); - previousEpisodes.AddRange(unairedEpisodes); + var parentIds = string.IsNullOrWhiteSpace(request.ParentId) ? new string[] { } : new[] { request.ParentId }; - var pagedItems = ApplyPaging(previousEpisodes, request.StartIndex, request.Limit); + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { "PremiereDate", "AirTime", "SortName" }, + SortOrder = SortOrder.Ascending, + MinPremiereDate = minPremiereDate, + StartIndex = request.StartIndex, + Limit = request.Limit + + }, parentIds); var options = GetDtoOptions(request); - var returnItems = _dtoService.GetBaseItemDtos(pagedItems, options, user).ToArray(); + var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, options, user).ToArray(); var result = new ItemsResult { - TotalRecordCount = itemsList.Count, + TotalRecordCount = itemsResult.TotalRecordCount, Items = returnItems }; @@ -440,7 +439,7 @@ namespace MediaBrowser.Api } episodes = season.GetEpisodes(user); - } + } else if (request.Season.HasValue) { var series = _libraryManager.GetItemById(request.Id) as Series; @@ -495,7 +494,7 @@ namespace MediaBrowser.Api .ToList(); var pagedItems = ApplyPaging(returnList, request.StartIndex, request.Limit); - + var dtoOptions = GetDtoOptions(request); var dtos = _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user) diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 23d4da60c..6867f6308 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -206,6 +206,8 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "Genres", Description = "Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Genres { get; set; } + public string GenreIds { get; set; } + [ApiMember(Name = "OfficialRatings", Description = "Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string OfficialRatings { get; set; } @@ -385,6 +387,11 @@ namespace MediaBrowser.Api.UserLibrary return (StudioIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); } + public string[] GetGenreIds() + { + return (GenreIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + } + public string[] GetPersonTypes() { return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 97d0ad7ab..761d10760 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -112,6 +112,15 @@ namespace MediaBrowser.Api.UserLibrary user == null ? _libraryManager.RootFolder : user.RootFolder : parentItem; + if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)) + { + item = user == null ? _libraryManager.RootFolder : user.RootFolder; + } + else if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) + { + item = user == null ? _libraryManager.RootFolder : user.RootFolder; + } + // Default list type = children if (!string.IsNullOrEmpty(request.Ids)) @@ -211,6 +220,7 @@ namespace MediaBrowser.Api.UserLibrary Tags = request.GetTags(), OfficialRatings = request.GetOfficialRatings(), Genres = request.GetGenres(), + GenreIds = request.GetGenreIds(), Studios = request.GetStudios(), StudioIds = request.GetStudioIds(), Person = request.Person, @@ -423,15 +433,6 @@ namespace MediaBrowser.Api.UserLibrary return false; } - // Min index number - if (request.MinIndexNumber.HasValue) - { - if (!(i.IndexNumber.HasValue && i.IndexNumber.Value >= request.MinIndexNumber.Value)) - { - return false; - } - } - // Min official rating if (!string.IsNullOrEmpty(request.MinOfficialRating)) { diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs index 9d7c38d6f..d29191db4 100644 --- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs +++ b/MediaBrowser.Api/UserLibrary/UserViewsService.cs @@ -26,6 +26,8 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "IncludeExternalContent", Description = "Whether or not to include external views such as channels or live tv", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool? IncludeExternalContent { get; set; } + + public string PresetViews { get; set; } } [Route("/Users/{UserId}/SpecialViewOptions", "GET")] @@ -75,9 +77,24 @@ namespace MediaBrowser.Api.UserLibrary query.IncludeExternalContent = request.IncludeExternalContent.Value; } + if (!string.IsNullOrWhiteSpace(request.PresetViews)) + { + query.PresetViews = request.PresetViews.Split(','); + } + + var app = AuthorizationContext.GetAuthorizationInfo(Request).Client ?? string.Empty; + if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1) + { + query.PresetViews = new[] { CollectionType.Music, CollectionType.Movies, CollectionType.TvShows }; + } + //query.PresetViews = new[] { CollectionType.Music, CollectionType.Movies, CollectionType.TvShows }; + var folders = await _userViewManager.GetUserViews(query, CancellationToken.None).ConfigureAwait(false); var dtoOptions = GetDtoOptions(request); + dtoOptions.Fields = new List<ItemFields>(); + dtoOptions.Fields.Add(ItemFields.PrimaryImageAspectRatio); + dtoOptions.Fields.Add(ItemFields.DisplayPreferencesId); var user = _userManager.GetUserById(request.UserId); @@ -123,7 +140,7 @@ namespace MediaBrowser.Api.UserLibrary var views = user.RootFolder .GetChildren(user, true) .OfType<Folder>() - .Where(i => !UserView.IsExcludedFromGrouping(i)) + .Where(UserView.IsEligibleForGrouping) .ToList(); var list = views @@ -141,9 +158,7 @@ namespace MediaBrowser.Api.UserLibrary private bool IsEligibleForSpecialView(ICollectionFolder view) { - var types = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Games, CollectionType.Music, CollectionType.Photos }; - - return types.Contains(view.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + return UserView.IsEligibleForEnhancedView(view.CollectionType); } } diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index 2a93efcde..caf8f54a6 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -453,7 +453,7 @@ namespace MediaBrowser.Common.Implementations RegisterSingleInstance<IApplicationPaths>(ApplicationPaths); - TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, Logger, FileSystemManager); + TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LogManager.GetLogger("TaskManager"), FileSystemManager); RegisterSingleInstance(JsonSerializer); RegisterSingleInstance(XmlSerializer); @@ -465,7 +465,7 @@ namespace MediaBrowser.Common.Implementations RegisterSingleInstance(FileSystemManager); - HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, Logger, FileSystemManager); + HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager); RegisterSingleInstance(HttpClient); NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager")); @@ -474,7 +474,7 @@ namespace MediaBrowser.Common.Implementations SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager); RegisterSingleInstance(SecurityManager); - InstallationManager = new InstallationManager(Logger, this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager); + InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager); RegisterSingleInstance(InstallationManager); ZipClient = new ZipClient(FileSystemManager); diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs index 845c984fb..b5566650c 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -55,6 +55,25 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks private ILogger Logger { get; set; } private readonly IFileSystem _fileSystem; + private bool _suspendTriggers; + + public bool SuspendTriggers + { + get { return _suspendTriggers; } + set + { + Logger.Info("Setting SuspendTriggers to {0}", value); + var executeQueued = _suspendTriggers && !value; + + _suspendTriggers = value; + + if (executeQueued) + { + ExecuteQueuedTasks(); + } + } + } + /// <summary> /// Initializes a new instance of the <see cref="TaskManager" /> class. /// </summary> @@ -151,6 +170,31 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks QueueScheduledTask<T>(new TaskExecutionOptions()); } + public void Execute<T>() + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + + if (scheduledTask == null) + { + Logger.Error("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name); + } + else + { + var type = scheduledTask.ScheduledTask.GetType(); + + Logger.Info("Queueing task {0}", type.Name); + + lock (_taskQueue) + { + if (scheduledTask.State == TaskState.Idle) + { + Execute(scheduledTask, new TaskExecutionOptions()); + } + } + } + } + /// <summary> /// Queues the scheduled task. /// </summary> @@ -183,7 +227,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks lock (_taskQueue) { - if (task.State == TaskState.Idle) + if (task.State == TaskState.Idle && !SuspendTriggers) { Execute(task, options); return; @@ -273,6 +317,13 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// </summary> private void ExecuteQueuedTasks() { + if (SuspendTriggers) + { + return; + } + + Logger.Info("ExecuteQueuedTasks"); + // Execute queued tasks lock (_taskQueue) { diff --git a/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs b/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs index 8d271859e..c1dc304f6 100644 --- a/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs +++ b/MediaBrowser.Common/ScheduledTasks/ITaskManager.cs @@ -66,7 +66,12 @@ namespace MediaBrowser.Common.ScheduledTasks void Cancel(IScheduledTaskWorker task); Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options = null); + void Execute<T>() + where T : IScheduledTask; + event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting; event EventHandler<TaskCompletionEventArgs> TaskCompleted; + + bool SuspendTriggers { get; set; } } }
\ No newline at end of file diff --git a/MediaBrowser.Controller/Channels/ChannelAudioItem.cs b/MediaBrowser.Controller/Channels/ChannelAudioItem.cs index 653cec901..17dcf138b 100644 --- a/MediaBrowser.Controller/Channels/ChannelAudioItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelAudioItem.cs @@ -18,9 +18,9 @@ namespace MediaBrowser.Controller.Channels public List<ChannelMediaInfo> ChannelMediaSources { get; set; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent); + return UnratedItem.ChannelContent; } protected override string CreateUserDataKey() diff --git a/MediaBrowser.Controller/Channels/ChannelFolderItem.cs b/MediaBrowser.Controller/Channels/ChannelFolderItem.cs index 9010470f8..f662020bb 100644 --- a/MediaBrowser.Controller/Channels/ChannelFolderItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelFolderItem.cs @@ -6,6 +6,7 @@ using System; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Channels @@ -20,6 +21,11 @@ namespace MediaBrowser.Controller.Channels return false; } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.ChannelContent; + } + [IgnoreDataMember] public override bool SupportsLocalMetadata { diff --git a/MediaBrowser.Controller/Channels/ChannelVideoItem.cs b/MediaBrowser.Controller/Channels/ChannelVideoItem.cs index fb545e57a..79ad4b36b 100644 --- a/MediaBrowser.Controller/Channels/ChannelVideoItem.cs +++ b/MediaBrowser.Controller/Channels/ChannelVideoItem.cs @@ -42,9 +42,9 @@ namespace MediaBrowser.Controller.Channels return ExternalId; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.ChannelContent); + return UnratedItem.ChannelContent; } [IgnoreDataMember] diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 77a81d0c8..5f0442f93 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities; +using System; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using System.Collections.Generic; @@ -44,10 +45,10 @@ namespace MediaBrowser.Controller.Dto /// <summary> /// Fills the synchronize information. /// </summary> - /// <param name="dtos">The dtos.</param> + /// <param name="tuples">The tuples.</param> /// <param name="options">The options.</param> /// <param name="user">The user.</param> - void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user); + void FillSyncInfo(IEnumerable<Tuple<BaseItem, BaseItemDto>> tuples, DtoOptions options, User user); /// <summary> /// Gets the base item dto. diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 43b980c20..766f1e5ed 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -27,9 +27,22 @@ namespace MediaBrowser.Controller.Entities.Audio public long? Size { get; set; } public string Container { get; set; } public int? TotalBitrate { get; set; } - public List<string> Tags { get; set; } public ExtraType? ExtraType { get; set; } + /// <summary> + /// Gets or sets the artist. + /// </summary> + /// <value>The artist.</value> + public List<string> Artists { get; set; } + + public List<string> AlbumArtists { get; set; } + + /// <summary> + /// Gets or sets the album. + /// </summary> + /// <value>The album.</value> + public string Album { get; set; } + [IgnoreDataMember] public bool IsThemeMedia { @@ -43,7 +56,6 @@ namespace MediaBrowser.Controller.Entities.Audio { Artists = new List<string>(); AlbumArtists = new List<string>(); - Tags = new List<string>(); } [IgnoreDataMember] @@ -92,14 +104,6 @@ namespace MediaBrowser.Controller.Entities.Audio locationType != LocationType.Virtual; } - /// <summary> - /// Gets or sets the artist. - /// </summary> - /// <value>The artist.</value> - public List<string> Artists { get; set; } - - public List<string> AlbumArtists { get; set; } - [IgnoreDataMember] public List<string> AllArtists { @@ -114,12 +118,6 @@ namespace MediaBrowser.Controller.Entities.Audio } } - /// <summary> - /// Gets or sets the album. - /// </summary> - /// <value>The album.</value> - public string Album { get; set; } - [IgnoreDataMember] public MusicAlbum AlbumEntity { @@ -173,9 +171,9 @@ namespace MediaBrowser.Controller.Entities.Audio return base.CreateUserDataKey(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return UnratedItem.Music; } public SongInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 654c9abd3..c5ce6a2f7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities.Audio { get { - return Parents.OfType<MusicArtist>().FirstOrDefault(); + return GetParents().OfType<MusicArtist>().FirstOrDefault(); } } @@ -114,13 +114,18 @@ namespace MediaBrowser.Controller.Entities.Audio return config.BlockUnratedItems.Contains(UnratedItem.Music); } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + public AlbumInfo GetLookupInfo() { var id = GetItemLookupInfo<AlbumInfo>(); id.AlbumArtists = AlbumArtists; - var artist = Parents.OfType<MusicArtist>().FirstOrDefault(); + var artist = GetParents().OfType<MusicArtist>().FirstOrDefault(); if (artist != null) { diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index f6d1d32a4..02bcceada 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -138,6 +138,11 @@ namespace MediaBrowser.Controller.Entities.Audio return config.BlockUnratedItems.Contains(UnratedItem.Music); } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Music; + } + public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) { var items = GetRecursiveChildren().ToList(); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 59e2b87e3..d52e2b37f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; @@ -24,6 +23,7 @@ using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Controller.Entities { @@ -34,6 +34,7 @@ namespace MediaBrowser.Controller.Entities { protected BaseItem() { + Tags = new List<string>(); Genres = new List<string>(); Studios = new List<string>(); ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); @@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// The supported image extensions /// </summary> - public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" }; + public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList(); @@ -103,7 +104,8 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name. /// </summary> /// <value>The name.</value> - public string Name + [IgnoreDataMember] + public virtual string Name { get { @@ -122,15 +124,24 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the id. /// </summary> /// <value>The id.</value> + [IgnoreDataMember] public Guid Id { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is hd. /// </summary> /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> + [IgnoreDataMember] public bool? IsHD { get; set; } /// <summary> + /// Gets or sets the audio. + /// </summary> + /// <value>The audio.</value> + [IgnoreDataMember] + public ProgramAudio? Audio { get; set; } + + /// <summary> /// Return the id that should be used to key display prefs for this item. /// Default is based on the type for everything except actual generic folders. /// </summary> @@ -149,6 +160,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> + [IgnoreDataMember] public virtual string Path { get; set; } [IgnoreDataMember] @@ -173,7 +185,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Id of the program. + /// If this content came from an external service, the id of the content on that service /// </summary> [IgnoreDataMember] public string ExternalId @@ -201,11 +213,6 @@ namespace MediaBrowser.Controller.Entities } } - public virtual bool IsHiddenFromUser(User user) - { - return false; - } - [IgnoreDataMember] public virtual bool IsOwnedItem { @@ -325,12 +332,14 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> + [IgnoreDataMember] public DateTime DateCreated { get; set; } /// <summary> /// Gets or sets the date modified. /// </summary> /// <value>The date modified.</value> + [IgnoreDataMember] public DateTime DateModified { get; set; } public DateTime DateLastSaved { get; set; } @@ -407,6 +416,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name of the forced sort. /// </summary> /// <value>The name of the forced sort.</value> + [IgnoreDataMember] public string ForcedSortName { get { return _forcedSortName; } @@ -447,10 +457,7 @@ namespace MediaBrowser.Controller.Entities { var idString = Id.ToString("N"); - if (ConfigurationManager.Configuration.EnableLibraryMetadataSubFolder) - { - basePath = System.IO.Path.Combine(basePath, "library"); - } + basePath = System.IO.Path.Combine(basePath, "library"); return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); } @@ -493,6 +500,7 @@ namespace MediaBrowser.Controller.Entities return sortable; } + [IgnoreDataMember] public Guid ParentId { get; set; } /// <summary> @@ -502,15 +510,7 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public Folder Parent { - get - { - if (ParentId != Guid.Empty) - { - return LibraryManager.GetItemById(ParentId) as Folder; - } - - return null; - } + get { return GetParent() as Folder; } set { @@ -525,16 +525,28 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] public IEnumerable<Folder> Parents { - get + get { return GetParents().OfType<Folder>(); } + } + + public BaseItem GetParent() + { + if (ParentId != Guid.Empty) { - var parent = Parent; + return LibraryManager.GetItemById(ParentId); + } - while (parent != null) - { - yield return parent; + return null; + } - parent = parent.Parent; - } + public IEnumerable<BaseItem> GetParents() + { + var parent = GetParent(); + + while (parent != null) + { + yield return parent; + + parent = parent.GetParent(); } } @@ -546,19 +558,20 @@ namespace MediaBrowser.Controller.Entities public T FindParent<T>() where T : Folder { - return Parents.OfType<T>().FirstOrDefault(); + return GetParents().OfType<T>().FirstOrDefault(); } [IgnoreDataMember] public virtual BaseItem DisplayParent { - get { return Parent; } + get { return GetParent(); } } /// <summary> /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// </summary> /// <value>The premiere date.</value> + [IgnoreDataMember] public DateTime? PremiereDate { get; set; } /// <summary> @@ -572,31 +585,35 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the display type of the media. /// </summary> /// <value>The display type of the media.</value> + [IgnoreDataMember] public string DisplayMediaType { get; set; } /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> + [IgnoreDataMember] public string OfficialRating { get; set; } /// <summary> /// Gets or sets the official rating description. /// </summary> /// <value>The official rating description.</value> + [IgnoreDataMember] public string OfficialRatingDescription { get; set; } /// <summary> /// Gets or sets the custom rating. /// </summary> /// <value>The custom rating.</value> - //[IgnoreDataMember] + [IgnoreDataMember] public string CustomRating { get; set; } /// <summary> /// Gets or sets the overview. /// </summary> /// <value>The overview.</value> + [IgnoreDataMember] public string Overview { get; set; } /// <summary> @@ -609,37 +626,48 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the genres. /// </summary> /// <value>The genres.</value> + [IgnoreDataMember] public List<string> Genres { get; set; } /// <summary> + /// Gets or sets the tags. + /// </summary> + /// <value>The tags.</value> + public List<string> Tags { get; set; } + + /// <summary> /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> + [IgnoreDataMember] public string HomePageUrl { get; set; } /// <summary> /// Gets or sets the community rating. /// </summary> /// <value>The community rating.</value> - //[IgnoreDataMember] + [IgnoreDataMember] public float? CommunityRating { get; set; } /// <summary> /// Gets or sets the community rating vote count. /// </summary> /// <value>The community rating vote count.</value> + [IgnoreDataMember] public int? VoteCount { get; set; } /// <summary> /// Gets or sets the run time ticks. /// </summary> /// <value>The run time ticks.</value> + [IgnoreDataMember] public long? RunTimeTicks { get; set; } /// <summary> /// Gets or sets the production year. /// </summary> /// <value>The production year.</value> + [IgnoreDataMember] public int? ProductionYear { get; set; } /// <summary> @@ -647,19 +675,34 @@ namespace MediaBrowser.Controller.Entities /// This could be episode number, album track number, etc. /// </summary> /// <value>The index number.</value> - //[IgnoreDataMember] + [IgnoreDataMember] public int? IndexNumber { get; set; } /// <summary> /// For an episode this could be the season number, or for a song this could be the disc number. /// </summary> /// <value>The parent index number.</value> + [IgnoreDataMember] public int? ParentIndexNumber { get; set; } [IgnoreDataMember] - public virtual string OfficialRatingForComparison + public string OfficialRatingForComparison { - get { return OfficialRating; } + get + { + if (!string.IsNullOrWhiteSpace(OfficialRating)) + { + return OfficialRating; + } + + var parent = DisplayParent; + if (parent != null) + { + return parent.OfficialRatingForComparison; + } + + return null; + } } [IgnoreDataMember] @@ -721,21 +764,21 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.ResolvePaths(files, directoryService, null) .OfType<Audio.Audio>() .Select(audio => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio; - - if (dbItem != null) { - audio = dbItem; - } + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(audio.Id) as Audio.Audio; - audio.ExtraType = ExtraType.ThemeSong; + if (dbItem != null) + { + audio = dbItem; + } + + audio.ExtraType = ExtraType.ThemeSong; - return audio; + return audio; - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToList(); + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } /// <summary> @@ -751,21 +794,21 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.ResolvePaths(files, directoryService, null) .OfType<Video>() .Select(item => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = LibraryManager.GetItemById(item.Id) as Video; - - if (dbItem != null) { - item = dbItem; - } + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = LibraryManager.GetItemById(item.Id) as Video; - item.ExtraType = ExtraType.ThemeVideo; + if (dbItem != null) + { + item = dbItem; + } - return item; + item.ExtraType = ExtraType.ThemeVideo; - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToList(); + return item; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } public Task RefreshMetadata(CancellationToken cancellationToken) @@ -821,7 +864,7 @@ namespace MediaBrowser.Controller.Entities [IgnoreDataMember] protected virtual bool SupportsOwnedItems { - get { return IsFolder || Parent != null; } + get { return IsFolder || GetParent() != null; } } [IgnoreDataMember] @@ -846,7 +889,7 @@ namespace MediaBrowser.Controller.Entities var localTrailersChanged = false; - if (LocationType == LocationType.FileSystem && Parent != null) + if (LocationType == LocationType.FileSystem && GetParent() != null) { var hasThemeMedia = this as IHasThemeMedia; if (hasThemeMedia != null) @@ -1008,7 +1051,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrWhiteSpace(lang)) { - lang = Parents + lang = GetParents() .Select(i => i.PreferredMetadataLanguage) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); } @@ -1038,7 +1081,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrWhiteSpace(lang)) { - lang = Parents + lang = GetParents() .Select(i => i.PreferredMetadataCountryCode) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); } @@ -1120,6 +1163,23 @@ namespace MediaBrowser.Controller.Entities public int? GetParentalRatingValue() { + var rating = CustomRating; + + if (string.IsNullOrWhiteSpace(rating)) + { + rating = OfficialRating; + } + + if (string.IsNullOrWhiteSpace(rating)) + { + return null; + } + + return LocalizationManager.GetRatingLevel(rating); + } + + public int? GetInheritedParentalRatingValue() + { var rating = CustomRatingForComparison; if (string.IsNullOrWhiteSpace(rating)) @@ -1156,6 +1216,11 @@ namespace MediaBrowser.Controller.Entities return true; } + public virtual UnratedItem GetBlockUnratedType() + { + return UnratedItem.Other; + } + /// <summary> /// Gets the block unrated value. /// </summary> @@ -1174,7 +1239,7 @@ namespace MediaBrowser.Controller.Entities return false; } - return config.BlockUnratedItems.Contains(UnratedItem.Other); + return config.BlockUnratedItems.Contains(GetBlockUnratedType()); } /// <summary> @@ -1206,14 +1271,14 @@ namespace MediaBrowser.Controller.Entities return false; } - if (Parents.Any(i => !i.IsVisible(user))) + if (GetParents().Any(i => !i.IsVisible(user))) { return false; } if (checkFolders) { - var topParent = Parents.LastOrDefault() ?? this; + var topParent = GetParents().LastOrDefault() ?? this; if (string.IsNullOrWhiteSpace(topParent.Path)) { @@ -1308,15 +1373,6 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Adds a person to the item - /// </summary> - /// <param name="person">The person.</param> - /// <exception cref="System.ArgumentNullException"></exception> - public void AddPerson(PersonInfo person) - { - } - - /// <summary> /// Adds a studio to the item /// </summary> /// <param name="name">The name.</param> @@ -1779,8 +1835,8 @@ namespace MediaBrowser.Controller.Entities ProviderIds = ProviderIds, IndexNumber = IndexNumber, ParentIndexNumber = ParentIndexNumber, - Year = ProductionYear, - PremiereDate = PremiereDate + Year = ProductionYear, + PremiereDate = PremiereDate }; } @@ -1875,5 +1931,59 @@ namespace MediaBrowser.Controller.Entities DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) }; } + + public virtual IEnumerable<Guid> GetAncestorIds() + { + return GetParents().Select(i => i.Id).Concat(LibraryManager.GetCollectionFolders(this).Select(i => i.Id)); + } + + public BaseItem GetTopParent() + { + if (IsTopParent) + { + return this; + } + + return GetParents().FirstOrDefault(i => i.IsTopParent); + } + + [IgnoreDataMember] + public virtual bool IsTopParent + { + get + { + if (GetParent() is AggregateFolder || this is Channel || this is BasePluginFolder) + { + return true; + } + + var view = this as UserView; + if (view != null && string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + } + + [IgnoreDataMember] + public virtual bool SupportsAncestors + { + get + { + return true; + } + } + + public virtual IEnumerable<Guid> GetIdsForAncestorQuery() + { + return new[] { Id }; + } + + public virtual Task Delete(DeleteOptions options) + { + return LibraryManager.DeleteItem(this, options); + } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index d31675baf..f006fedd2 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -17,19 +17,8 @@ namespace MediaBrowser.Controller.Entities } } - /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public List<string> Tags { get; set; } - public string SeriesName { get; set; } - public Book() - { - Tags = new List<string>(); - } - public override bool CanDownload() { var locationType = LocationType; @@ -37,9 +26,9 @@ namespace MediaBrowser.Controller.Entities locationType != LocationType.Virtual; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Book); + return UnratedItem.Book; } public BookInfo GetLookupInfo() @@ -48,7 +37,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(SeriesName)) { - info.SeriesName = Parents.Select(i => i.Name).FirstOrDefault(); + info.SeriesName = GetParents().Select(i => i.Name).FirstOrDefault(); } else { diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 0da253186..b2c7c2fa8 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -181,9 +181,7 @@ namespace MediaBrowser.Controller.Entities } private List<LinkedChild> GetLinkedChildrenInternal() { - return LibraryManager.RootFolder.Children - .OfType<Folder>() - .Where(i => i.Path != null && PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase)) + return GetPhysicalParents() .SelectMany(c => c.LinkedChildren) .ToList(); } @@ -199,11 +197,14 @@ namespace MediaBrowser.Controller.Entities private IEnumerable<BaseItem> GetActualChildren() { - return - LibraryManager.RootFolder.Children + return GetPhysicalParents().SelectMany(c => c.Children); + } + + public IEnumerable<Folder> GetPhysicalParents() + { + return LibraryManager.RootFolder.Children .OfType<Folder>() - .Where(i => i.Path != null && PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase)) - .SelectMany(c => c.Children); + .Where(i => i.Path != null && PhysicalLocations.Contains(i.Path, StringComparer.OrdinalIgnoreCase)); } [IgnoreDataMember] diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 9ea1b64c0..824a067ad 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -28,7 +28,6 @@ namespace MediaBrowser.Controller.Entities public List<Guid> ThemeSongIds { get; set; } public List<Guid> ThemeVideoIds { get; set; } - public List<string> Tags { get; set; } public Folder() { @@ -36,7 +35,6 @@ namespace MediaBrowser.Controller.Entities ThemeSongIds = new List<Guid>(); ThemeVideoIds = new List<Guid>(); - Tags = new List<string>(); } [IgnoreDataMember] @@ -151,7 +149,15 @@ namespace MediaBrowser.Controller.Entities await LibraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); - await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + if (!EnableNewFolderQuerying()) + { + await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + } + } + + private static bool EnableNewFolderQuerying() + { + return ConfigurationManager.Configuration.MigrationVersion >= 1; } protected void AddChildrenInternal(IEnumerable<BaseItem> children) @@ -196,21 +202,6 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] - public override string OfficialRatingForComparison - { - get - { - // Never want folders to be blocked by "BlockNotRated" - if (this is Series) - { - return base.OfficialRatingForComparison; - } - - return !string.IsNullOrWhiteSpace(base.OfficialRatingForComparison) ? base.OfficialRatingForComparison : "None"; - } - } - /// <summary> /// Removes the child. /// </summary> @@ -224,7 +215,12 @@ namespace MediaBrowser.Controller.Entities item.SetParent(null); - return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); + if (!EnableNewFolderQuerying()) + { + return ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken); + } + + return Task.FromResult(true); } /// <summary> @@ -457,32 +453,25 @@ namespace MediaBrowser.Controller.Entities { BaseItem currentChild; - if (currentChildren.TryGetValue(child.Id, out currentChild)) + if (currentChildren.TryGetValue(child.Id, out currentChild) && IsValidFromResolver(currentChild, child)) { - if (IsValidFromResolver(currentChild, child)) + var currentChildLocationType = currentChild.LocationType; + if (currentChildLocationType != LocationType.Remote && + currentChildLocationType != LocationType.Virtual) { - var currentChildLocationType = currentChild.LocationType; - if (currentChildLocationType != LocationType.Remote && - currentChildLocationType != LocationType.Virtual) - { - currentChild.DateModified = child.DateModified; - } - - await UpdateIsOffline(currentChild, false).ConfigureAwait(false); - validChildren.Add(currentChild); - } - else - { - newItems.Add(child); - validChildren.Add(child); + currentChild.DateModified = child.DateModified; } + + await UpdateIsOffline(currentChild, false).ConfigureAwait(false); + validChildren.Add(currentChild); + + continue; } - else - { - // Brand new item - needs to be added - newItems.Add(child); - validChildren.Add(child); - } + + // Brand new item - needs to be added + child.SetParent(this); + newItems.Add(child); + validChildren.Add(child); } // If any items were added or removed.... @@ -508,7 +497,6 @@ namespace MediaBrowser.Controller.Entities } else { - await UpdateIsOffline(item, false).ConfigureAwait(false); actualRemovals.Add(item); } } @@ -519,6 +507,11 @@ namespace MediaBrowser.Controller.Entities foreach (var item in actualRemovals) { + Logger.Debug("Removed item: " + item.Path); + + item.SetParent(null); + item.IsOffline = false; + await LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }).ConfigureAwait(false); LibraryManager.ReportItemRemoved(item); } } @@ -527,7 +520,10 @@ namespace MediaBrowser.Controller.Entities AddChildrenInternal(newItems); - await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + if (!EnableNewFolderQuerying()) + { + await ItemRepository.SaveChildren(Id, ActualChildren.Select(i => i.Id).ToList(), cancellationToken).ConfigureAwait(false); + } } } @@ -721,7 +717,7 @@ namespace MediaBrowser.Controller.Entities return true; } - return ContainsPath(LibraryManager.GetVirtualFolders(), originalPath); + return false; } /// <summary> @@ -757,19 +753,16 @@ namespace MediaBrowser.Controller.Entities /// <returns>IEnumerable{BaseItem}.</returns> protected IEnumerable<BaseItem> GetCachedChildren() { - if (ConfigurationManager.Configuration.DisableStartupScan) + if (EnableNewFolderQuerying()) { - return ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null); - //return ItemRepository.GetItems(new InternalItemsQuery - //{ - // ParentId = Id + return ItemRepository.GetItemList(new InternalItemsQuery + { + ParentId = Id - //}).Items.Select(RetrieveChild).Where(i => i != null); - } - else - { - return ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null); + }).Select(RetrieveChild).Where(i => i != null); } + + return ItemRepository.GetChildrenItems(Id).Select(RetrieveChild).Where(i => i != null); } private BaseItem RetrieveChild(BaseItem child) @@ -832,20 +825,8 @@ namespace MediaBrowser.Controller.Entities return UserViewBuilder.PostFilterAndSort(items, this, null, query, LibraryManager); } - /// <summary> - /// Gets allowed children of an item - /// </summary> - /// <param name="user">The user.</param> - /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> - /// <returns>IEnumerable{BaseItem}.</returns> - /// <exception cref="System.ArgumentNullException"></exception> public virtual IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren) { - return GetChildren(user, includeLinkedChildren, false); - } - - internal IEnumerable<BaseItem> GetChildren(User user, bool includeLinkedChildren, bool includeHidden) - { if (user == null) { throw new ArgumentNullException(); @@ -856,7 +837,7 @@ namespace MediaBrowser.Controller.Entities var result = new Dictionary<Guid, BaseItem>(); - AddChildren(user, includeLinkedChildren, result, includeHidden, false, null); + AddChildren(user, includeLinkedChildren, result, false, null); return result.Values; } @@ -872,29 +853,25 @@ namespace MediaBrowser.Controller.Entities /// <param name="user">The user.</param> /// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param> /// <param name="result">The result.</param> - /// <param name="includeHidden">if set to <c>true</c> [include hidden].</param> /// <param name="recursive">if set to <c>true</c> [recursive].</param> /// <param name="filter">The filter.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool includeHidden, bool recursive, Func<BaseItem, bool> filter) + private void AddChildren(User user, bool includeLinkedChildren, Dictionary<Guid, BaseItem> result, bool recursive, Func<BaseItem, bool> filter) { foreach (var child in GetEligibleChildrenForRecursiveChildren(user)) { if (child.IsVisible(user)) { - if (includeHidden || !child.IsHiddenFromUser(user)) + if (filter == null || filter(child)) { - if (filter == null || filter(child)) - { - result[child.Id] = child; - } + result[child.Id] = child; } if (recursive && child.IsFolder) { var folder = (Folder)child; - folder.AddChildren(user, includeLinkedChildren, result, includeHidden, true, filter); + folder.AddChildren(user, includeLinkedChildren, result, true, filter); } } } @@ -935,7 +912,7 @@ namespace MediaBrowser.Controller.Entities var result = new Dictionary<Guid, BaseItem>(); - AddChildren(user, true, result, false, true, filter); + AddChildren(user, true, result, true, filter); return result.Values; } @@ -1184,6 +1161,7 @@ namespace MediaBrowser.Controller.Entities Recursive = true, IsFolder = false, IsUnaired = false + }; if (!user.Configuration.DisplayMissingEpisodes) @@ -1322,4 +1300,4 @@ namespace MediaBrowser.Controller.Entities } } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/Game.cs b/MediaBrowser.Controller/Entities/Game.cs index ed3e85d58..e073d09f6 100644 --- a/MediaBrowser.Controller/Entities/Game.cs +++ b/MediaBrowser.Controller/Entities/Game.cs @@ -21,7 +21,6 @@ namespace MediaBrowser.Controller.Entities RemoteTrailerIds = new List<Guid>(); ThemeSongIds = new List<Guid>(); ThemeVideoIds = new List<Guid>(); - Tags = new List<string>(); } public List<Guid> LocalTrailerIds { get; set; } @@ -35,12 +34,6 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public List<string> Tags { get; set; } - - /// <summary> /// Gets or sets the remote trailers. /// </summary> /// <value>The remote trailers.</value> @@ -105,9 +98,9 @@ namespace MediaBrowser.Controller.Entities return base.GetDeletePaths(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Game); + return UnratedItem.Game; } public GameInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/GameSystem.cs b/MediaBrowser.Controller/Entities/GameSystem.cs index 35f7e3350..bc35c4738 100644 --- a/MediaBrowser.Controller/Entities/GameSystem.cs +++ b/MediaBrowser.Controller/Entities/GameSystem.cs @@ -50,6 +50,11 @@ namespace MediaBrowser.Controller.Entities return false; } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Game; + } + public GameSystemInfo GetLookupInfo() { var id = GetItemLookupInfo<GameSystemInfo>(); diff --git a/MediaBrowser.Controller/Entities/ICollectionFolder.cs b/MediaBrowser.Controller/Entities/ICollectionFolder.cs index b55ca0a17..f4544f173 100644 --- a/MediaBrowser.Controller/Entities/ICollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/ICollectionFolder.cs @@ -16,6 +16,11 @@ namespace MediaBrowser.Controller.Entities IEnumerable<string> PhysicalLocations { get; } } + public interface ISupportsUserSpecificView + { + bool EnableUserSpecificView { get; } + } + public static class CollectionFolderExtensions { public static string GetViewType(this ICollectionFolder folder, User user) diff --git a/MediaBrowser.Controller/Entities/IHiddenFromDisplay.cs b/MediaBrowser.Controller/Entities/IHiddenFromDisplay.cs new file mode 100644 index 000000000..82d581fcf --- /dev/null +++ b/MediaBrowser.Controller/Entities/IHiddenFromDisplay.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Entities +{ + public interface IHiddenFromDisplay + { + /// <summary> + /// Determines whether the specified user is hidden. + /// </summary> + /// <param name="user">The user.</param> + /// <returns><c>true</c> if the specified user is hidden; otherwise, <c>false</c>.</returns> + bool IsHiddenFromUser(User user); + } +} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 785e2fd2b..0595d0569 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -1,6 +1,7 @@ using MediaBrowser.Model.Entities; using System; using System.Collections.Generic; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Entities { @@ -26,10 +27,12 @@ namespace MediaBrowser.Controller.Entities public bool? IsLiked { get; set; } public bool? IsPlayed { get; set; } public bool? IsResumable { get; set; } + public bool? IncludeItemsByName { get; set; } public string[] MediaTypes { get; set; } public string[] IncludeItemTypes { get; set; } public string[] ExcludeItemTypes { get; set; } + public string[] ExcludeTags { get; set; } public string[] Genres { get; set; } public bool? IsMissing { get; set; } @@ -69,12 +72,15 @@ namespace MediaBrowser.Controller.Entities public string[] Studios { get; set; } public string[] StudioIds { get; set; } + public string[] GenreIds { get; set; } public ImageType[] ImageTypes { get; set; } public VideoType[] VideoTypes { get; set; } + public UnratedItem[] BlockUnratedItems { get; set; } public int[] Years { get; set; } public string[] Tags { get; set; } public string[] OfficialRatings { get; set; } + public DateTime? MinPremiereDate { get; set; } public DateTime? MinStartDate { get; set; } public DateTime? MaxStartDate { get; set; } public DateTime? MinEndDate { get; set; } @@ -87,6 +93,7 @@ namespace MediaBrowser.Controller.Entities public int? MinPlayers { get; set; } public int? MaxPlayers { get; set; } + public int? MinIndexNumber { get; set; } public double? MinCriticRating { get; set; } public double? MinCommunityRating { get; set; } @@ -101,9 +108,14 @@ namespace MediaBrowser.Controller.Entities public LocationType? LocationType { get; set; } public Guid? ParentId { get; set; } + public string[] AncestorIds { get; set; } + public string[] TopParentIds { get; set; } + + public LocationType[] ExcludeLocationTypes { get; set; } public InternalItemsQuery() { + BlockUnratedItems = new UnratedItem[] { }; Tags = new string[] { }; OfficialRatings = new string[] { }; SortBy = new string[] { }; @@ -113,6 +125,7 @@ namespace MediaBrowser.Controller.Entities Genres = new string[] { }; Studios = new string[] { }; StudioIds = new string[] { }; + GenreIds = new string[] { }; ImageTypes = new ImageType[] { }; VideoTypes = new VideoType[] { }; Years = new int[] { }; @@ -120,6 +133,29 @@ namespace MediaBrowser.Controller.Entities PersonIds = new string[] { }; ChannelIds = new string[] { }; ItemIds = new string[] { }; + AncestorIds = new string[] { }; + TopParentIds = new string[] { }; + ExcludeTags = new string[] { }; + ExcludeLocationTypes = new LocationType[] { }; + } + + public InternalItemsQuery(User user) + : this() + { + if (user != null) + { + var policy = user.Policy; + MaxParentalRating = policy.MaxParentalRating; + + if (policy.MaxParentalRating.HasValue) + { + BlockUnratedItems = policy.BlockUnratedItems; + } + + ExcludeTags = policy.BlockedTags; + + User = user; + } } } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 9317f688f..cd3e07ea3 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -8,15 +8,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; -using System.Threading; -using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities.Movies { /// <summary> /// Class BoxSet /// </summary> - public class BoxSet : Folder, IHasTrailers, IHasKeywords, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo>, IMetadataContainer, IHasShares + public class BoxSet : Folder, IHasTrailers, IHasKeywords, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo>, IHasShares { public List<Share> Shares { get; set; } @@ -65,6 +63,11 @@ namespace MediaBrowser.Controller.Entities.Movies return config.BlockUnratedItems.Contains(UnratedItem.Movie); } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Movie; + } + [IgnoreDataMember] public override bool IsPreSorted { @@ -154,34 +157,6 @@ namespace MediaBrowser.Controller.Entities.Movies return GetItemLookupInfo<BoxSetInfo>(); } - public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) - { - // Refresh bottom up, children first, then the boxset - // By then hopefully the movies within will have Tmdb collection values - var items = GetRecursiveChildren().ToList(); - - var totalItems = items.Count; - var numComplete = 0; - - // Refresh songs - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= totalItems; - progress.Report(percent * 100); - } - - // Refresh current item - await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - - progress.Report(100); - } - public override bool IsVisible(User user) { var userId = user.Id.ToString("N"); diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 1a8148edf..749d562ac 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -130,7 +130,7 @@ namespace MediaBrowser.Controller.Entities.Movies // Must have a parent to have special features // In other words, it must be part of the Parent/Child tree - if (LocationType == LocationType.FileSystem && Parent != null && !IsInMixedFolder) + if (LocationType == LocationType.FileSystem && GetParent() != null && !IsInMixedFolder) { var specialFeaturesChanged = await RefreshSpecialFeatures(options, fileSystemChildren, cancellationToken).ConfigureAwait(false); @@ -159,9 +159,9 @@ namespace MediaBrowser.Controller.Entities.Movies return itemsChanged; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Movie); + return UnratedItem.Movie; } public MovieInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index b2cad02de..8a820b5ff 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -56,9 +56,9 @@ namespace MediaBrowser.Controller.Entities return this.GetProviderId(MetadataProviders.Tmdb) ?? this.GetProviderId(MetadataProviders.Imdb) ?? base.CreateUserDataKey(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Music); + return UnratedItem.Music; } public MusicVideoInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index a365b99c6..560ea6e05 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -102,6 +102,15 @@ namespace MediaBrowser.Controller.Entities return false; } } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } } /// <summary> diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index a3d892181..aa9e44297 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -9,12 +9,10 @@ namespace MediaBrowser.Controller.Entities { public class Photo : BaseItem, IHasTags, IHasTaglines { - public List<string> Tags { get; set; } public List<string> Taglines { get; set; } public Photo() { - Tags = new List<string>(); Taglines = new List<string>(); } @@ -51,10 +49,15 @@ namespace MediaBrowser.Controller.Entities { get { - return Parents.OfType<PhotoAlbum>().FirstOrDefault(); + return GetParents().OfType<PhotoAlbum>().FirstOrDefault(); } } + public override bool CanDownload() + { + return true; + } + public int? Width { get; set; } public int? Height { get; set; } public string CameraMake { get; set; } @@ -70,10 +73,5 @@ namespace MediaBrowser.Controller.Entities public double? Longitude { get; set; } public double? Altitude { get; set; } public int? IsoSpeedRating { get; set; } - - protected override bool GetBlockUnratedValue(UserPolicy config) - { - return config.BlockUnratedItems.Contains(UnratedItem.Other); - } } } diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 5b48a70e9..1f4faaf49 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -9,8 +9,9 @@ using System.Threading.Tasks; namespace MediaBrowser.Controller.Entities { - public class PhotoAlbum : Folder, IMetadataContainer + public class PhotoAlbum : Folder { + [IgnoreDataMember] public override bool SupportsLocalMetadata { get @@ -32,31 +33,5 @@ namespace MediaBrowser.Controller.Entities { return config.BlockUnratedItems.Contains(UnratedItem.Other); } - - public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken) - { - var items = GetRecursiveChildren().ToList(); - - var totalItems = items.Count; - var numComplete = 0; - - // Refresh songs - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= totalItems; - progress.Report(percent * 100); - } - - // Refresh current item - await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - - progress.Report(100); - } } } diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index 822f305ed..a55527f37 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -10,13 +10,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Studio : BaseItem, IItemByName, IHasTags { - public List<string> Tags { get; set; } - - public Studio() - { - Tags = new List<string>(); - } - /// <summary> /// Gets the user data key. /// </summary> diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 92ca9e970..d4f829917 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.Entities.TV { get { - return Season ?? Parent; + return Season ?? GetParent(); } } @@ -116,19 +116,6 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// Our rating comes from our series - /// </summary> - [IgnoreDataMember] - public override string OfficialRatingForComparison - { - get - { - var series = Series; - return series != null ? series.OfficialRatingForComparison : base.OfficialRatingForComparison; - } - } - - /// <summary> /// This Episode's Series Instance /// </summary> /// <value>The series.</value> @@ -265,14 +252,28 @@ namespace MediaBrowser.Controller.Entities.TV } } + public override IEnumerable<Guid> GetAncestorIds() + { + var list = base.GetAncestorIds().ToList(); + + var seasonId = SeasonId; + + if (seasonId.HasValue && !list.Contains(seasonId.Value)) + { + list.Add(seasonId.Value); + } + + return list; + } + public override IEnumerable<string> GetDeletePaths() { return new[] { Path }; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Series); + return UnratedItem.Series; } public EpisodeInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 21b89d7a9..93eac058d 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -6,6 +6,7 @@ using MoreLinq; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Controller.Entities.TV { @@ -32,7 +33,7 @@ namespace MediaBrowser.Controller.Entities.TV [IgnoreDataMember] public override BaseItem DisplayParent { - get { return Series ?? Parent; } + get { return Series ?? GetParent(); } } // Genre, Rating and Stuido will all be the same @@ -88,19 +89,6 @@ namespace MediaBrowser.Controller.Entities.TV } /// <summary> - /// Our rating comes from our series - /// </summary> - [IgnoreDataMember] - public override string OfficialRatingForComparison - { - get - { - var series = Series; - return series != null ? series.OfficialRatingForComparison : base.OfficialRatingForComparison; - } - } - - /// <summary> /// Creates the name of the sort. /// </summary> /// <returns>System.String.</returns> @@ -234,6 +222,11 @@ namespace MediaBrowser.Controller.Entities.TV return false; } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + [IgnoreDataMember] public string SeriesName { diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index b23833845..420b3c313 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -333,6 +333,11 @@ namespace MediaBrowser.Controller.Entities.TV return config.BlockUnratedItems.Contains(UnratedItem.Series); } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.Series; + } + public SeriesInfo GetLookupInfo() { var info = GetItemLookupInfo<SeriesInfo>(); diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 6ec512783..3c7d39e0d 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -73,7 +73,7 @@ namespace MediaBrowser.Controller.Entities get { // Local trailers are not part of children - return Parent == null; + return GetParent() == null; } } @@ -97,9 +97,9 @@ namespace MediaBrowser.Controller.Entities return base.CreateUserDataKey(); } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.Trailer); + return UnratedItem.Trailer; } public TrailerInfo GetLookupInfo() diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index be8521a5c..6c9266d74 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -20,7 +20,6 @@ namespace MediaBrowser.Controller.Entities { public static IUserManager UserManager { get; set; } public static IXmlSerializer XmlSerializer { get; set; } - public bool EnableUserViews { get; set; } /// <summary> /// From now on all user paths will be Id-based. @@ -58,6 +57,26 @@ namespace MediaBrowser.Controller.Entities } } + private string _name; + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get + { + return _name; + } + set + { + _name = value; + + // lazy load this again + SortName = null; + } + } + /// <summary> /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index a78beb645..b7946cb92 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -55,13 +55,21 @@ namespace MediaBrowser.Controller.Entities } } + protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) + { + var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList(); + list.AddRange(LibraryManager.RootFolder.VirtualChildren); + + return list; + } + /// <summary> /// Get the children of this folder from the actual file system /// </summary> /// <returns>IEnumerable{BaseItem}.</returns> protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService) { - return base.GetNonCachedChildren(directoryService).Concat(LibraryManager.RootFolder.VirtualChildren); + return base.GetNonCachedChildren(directoryService); } public override bool BeforeMetadataRefresh() diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 5ee49ae5a..41c19f11d 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Threading.Tasks; +using System.Linq; namespace MediaBrowser.Controller.Entities { @@ -16,7 +17,7 @@ namespace MediaBrowser.Controller.Entities public Guid DisplayParentId { get; set; } public Guid? UserId { get; set; } - + public static ITVSeriesManager TVSeriesManager; public static IPlaylistManager PlaylistManager; @@ -24,7 +25,26 @@ namespace MediaBrowser.Controller.Entities { return true; } - + + public override IEnumerable<Guid> GetIdsForAncestorQuery() + { + var list = new List<Guid>(); + + if (DisplayParentId != Guid.Empty) + { + list.Add(DisplayParentId); + } + else if (ParentId != Guid.Empty) + { + list.Add(ParentId); + } + else + { + list.Add(Id); + } + return list; + } + public override Task<QueryResult<BaseItem>> GetItems(InternalItemsQuery query) { var parent = this as Folder; @@ -81,16 +101,11 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, false); } - public static bool IsExcludedFromGrouping(Folder folder) + public static bool IsUserSpecific(Folder folder) { var standaloneTypes = new List<string> { - CollectionType.Books, - CollectionType.HomeVideos, - CollectionType.Photos, - CollectionType.Playlists, - CollectionType.BoxSets, - CollectionType.MusicVideos + CollectionType.Playlists }; var collectionFolder = folder as ICollectionFolder; @@ -100,25 +115,63 @@ namespace MediaBrowser.Controller.Entities return false; } + var supportsUserSpecific = folder as ISupportsUserSpecificView; + if (supportsUserSpecific != null && supportsUserSpecific.EnableUserSpecificView) + { + return true; + } + return standaloneTypes.Contains(collectionFolder.CollectionType ?? string.Empty); } - public static bool IsUserSpecific(Folder folder) + public static bool IsEligibleForGrouping(Folder folder) { - var standaloneTypes = new List<string> - { - CollectionType.Playlists, - CollectionType.BoxSets + var collectionFolder = folder as ICollectionFolder; + return collectionFolder != null && IsEligibleForGrouping(collectionFolder.CollectionType); + } + + public static bool IsEligibleForGrouping(string viewType) + { + var types = new[] + { + CollectionType.Movies, + CollectionType.TvShows, + string.Empty }; - var collectionFolder = folder as ICollectionFolder; + return types.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } - if (collectionFolder == null) - { - return false; - } + public static bool IsEligibleForEnhancedView(string viewType) + { + var types = new[] + { + CollectionType.Movies, + CollectionType.TvShows + }; - return standaloneTypes.Contains(collectionFolder.CollectionType ?? string.Empty); + return types.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + public static bool EnableOriginalFolder(string viewType) + { + var types = new[] + { + CollectionType.Games, + CollectionType.Books, + CollectionType.MusicVideos, + CollectionType.HomeVideos, + CollectionType.Photos, + CollectionType.Music, + CollectionType.BoxSets + }; + + return types.Contains(viewType ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + protected override Task ValidateChildrenInternal(IProgress<double> progress, System.Threading.CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, Providers.MetadataRefreshOptions refreshOptions, Providers.IDirectoryService directoryService) + { + return Task.FromResult(true); } [IgnoreDataMember] diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index f5800ce81..721ec3f1b 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -120,59 +120,34 @@ namespace MediaBrowser.Controller.Entities return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false); } + case CollectionType.Photos: case CollectionType.Books: case CollectionType.HomeVideos: + case CollectionType.Games: case CollectionType.MusicVideos: + { + if (query.Recursive) + { + return GetResult(queryParent.GetRecursiveChildren(user, true), queryParent, query); + } return GetResult(queryParent.GetChildren(user, true), queryParent, query); + } case CollectionType.Folders: return GetResult(user.RootFolder.GetChildren(user, true), queryParent, query); - case CollectionType.Games: - return await GetGameView(user, queryParent, query).ConfigureAwait(false); - case CollectionType.Playlists: return await GetPlaylistsView(queryParent, user, query).ConfigureAwait(false); case CollectionType.BoxSets: return await GetBoxsetView(queryParent, user, query).ConfigureAwait(false); - case CollectionType.Photos: - return await GetPhotosView(queryParent, user, query).ConfigureAwait(false); - case CollectionType.TvShows: return await GetTvView(queryParent, user, query).ConfigureAwait(false); - case CollectionType.Music: - return await GetMusicFolders(queryParent, user, query).ConfigureAwait(false); - case CollectionType.Movies: return await GetMovieFolders(queryParent, user, query).ConfigureAwait(false); - case SpecialFolder.MusicGenres: - return await GetMusicGenres(queryParent, user, query).ConfigureAwait(false); - - case SpecialFolder.MusicGenre: - return await GetMusicGenreItems(queryParent, displayParent, user, query).ConfigureAwait(false); - - case SpecialFolder.GameGenres: - return await GetGameGenres(queryParent, user, query).ConfigureAwait(false); - - case SpecialFolder.GameGenre: - return await GetGameGenreItems(queryParent, displayParent, user, query).ConfigureAwait(false); - - case SpecialFolder.GameSystems: - return GetGameSystems(queryParent, user, query); - - case SpecialFolder.LatestGames: - return GetLatestGames(queryParent, user, query); - - case SpecialFolder.RecentlyPlayedGames: - return GetRecentlyPlayedGames(queryParent, user, query); - - case SpecialFolder.GameFavorites: - return GetFavoriteGames(queryParent, user, query); - case SpecialFolder.TvShowSeries: return GetTvSeries(queryParent, user, query); @@ -212,6 +187,21 @@ namespace MediaBrowser.Controller.Entities case SpecialFolder.MovieCollections: return GetMovieCollections(queryParent, user, query); + case SpecialFolder.TvFavoriteEpisodes: + return GetFavoriteEpisodes(queryParent, user, query); + + case SpecialFolder.TvFavoriteSeries: + return GetFavoriteSeries(queryParent, user, query); + + case CollectionType.Music: + return await GetMusicFolders(queryParent, user, query).ConfigureAwait(false); + + case SpecialFolder.MusicGenres: + return await GetMusicGenres(queryParent, user, query).ConfigureAwait(false); + + case SpecialFolder.MusicGenre: + return await GetMusicGenreItems(queryParent, displayParent, user, query).ConfigureAwait(false); + case SpecialFolder.MusicLatest: return GetMusicLatest(queryParent, user, query); @@ -230,12 +220,6 @@ namespace MediaBrowser.Controller.Entities case SpecialFolder.MusicSongs: return GetMusicSongs(queryParent, user, query); - case SpecialFolder.TvFavoriteEpisodes: - return GetFavoriteEpisodes(queryParent, user, query); - - case SpecialFolder.TvFavoriteSeries: - return GetFavoriteSeries(queryParent, user, query); - case SpecialFolder.MusicFavorites: return await GetMusicFavorites(queryParent, user, query).ConfigureAwait(false); @@ -262,18 +246,6 @@ namespace MediaBrowser.Controller.Entities } } - private async Task<QueryResult<BaseItem>> FindPlaylists(Folder parent, User user, InternalItemsQuery query) - { - var list = _playlistManager.GetPlaylists(user.Id.ToString("N")); - - return GetResult(list, parent, query); - } - - private int GetSpecialItemsLimit() - { - return 50; - } - private async Task<QueryResult<BaseItem>> GetMusicFolders(Folder parent, User user, InternalItemsQuery query) { if (query.Recursive) @@ -289,7 +261,7 @@ namespace MediaBrowser.Controller.Entities list.Add(await GetUserView(SpecialFolder.MusicPlaylists, "1", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicAlbums, "2", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, "3", parent).ConfigureAwait(false)); - //list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false)); + list.Add(await GetUserView(SpecialFolder.MusicArtists, "4", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicSongs, "5", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicGenres, "6", parent).ConfigureAwait(false)); list.Add(await GetUserView(SpecialFolder.MusicFavorites, "7", parent).ConfigureAwait(false)); @@ -422,6 +394,36 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, parent, null, query); } + private QueryResult<BaseItem> GetFavoriteSongs(Folder parent, User user, InternalItemsQuery query) + { + query.IsFavorite = true; + + var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music }, i => (i is Audio.Audio) && FilterItem(i, query)); + + return PostFilterAndSort(items, parent, null, query); + } + + private QueryResult<BaseItem> GetFavoriteAlbums(Folder parent, User user, InternalItemsQuery query) + { + query.IsFavorite = true; + + var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music }, i => (i is MusicAlbum) && FilterItem(i, query)); + + return PostFilterAndSort(items, parent, null, query); + } + + private async Task<QueryResult<BaseItem>> FindPlaylists(Folder parent, User user, InternalItemsQuery query) + { + var list = _playlistManager.GetPlaylists(user.Id.ToString("N")); + + return GetResult(list, parent, query); + } + + private int GetSpecialItemsLimit() + { + return 50; + } + private async Task<QueryResult<BaseItem>> GetMovieFolders(Folder parent, User user, InternalItemsQuery query) { if (query.Recursive) @@ -480,24 +482,6 @@ namespace MediaBrowser.Controller.Entities return PostFilterAndSort(items, parent, null, query); } - private QueryResult<BaseItem> GetFavoriteSongs(Folder parent, User user, InternalItemsQuery query) - { - query.IsFavorite = true; - - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music }, i => (i is Audio.Audio) && FilterItem(i, query)); - - return PostFilterAndSort(items, parent, null, query); - } - - private QueryResult<BaseItem> GetFavoriteAlbums(Folder parent, User user, InternalItemsQuery query) - { - query.IsFavorite = true; - - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music }, i => (i is MusicAlbum) && FilterItem(i, query)); - - return PostFilterAndSort(items, parent, null, query); - } - private QueryResult<BaseItem> GetMovieMovies(Folder parent, User user, InternalItemsQuery query) { var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty }, i => (i is Movie) && FilterItem(i, query)); @@ -582,19 +566,6 @@ namespace MediaBrowser.Controller.Entities return GetResult(collections, parent, query); } - private async Task<QueryResult<BaseItem>> GetPhotosView(Folder queryParent, User user, InternalItemsQuery query) - { - if (query.Recursive) - { - var mediaTypes = new[] { MediaType.Video, MediaType.Photo }; - var items = GetRecursiveChildren(queryParent, user, new[] { CollectionType.Photos, string.Empty }, i => (i is PhotoAlbum || mediaTypes.Contains(i.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) && FilterItem(i, query)); - - return PostFilterAndSort(items, queryParent, null, query); - } - - return GetResult(queryParent.GetChildren(user, true), queryParent, query); - } - private async Task<QueryResult<BaseItem>> GetTvView(Folder parent, User user, InternalItemsQuery query) { if (query.Recursive) @@ -617,54 +588,6 @@ namespace MediaBrowser.Controller.Entities return GetResult(list, parent, query); } - private async Task<QueryResult<BaseItem>> GetGameView(User user, Folder parent, InternalItemsQuery query) - { - if (query.Recursive) - { - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }, i => FilterItem(i, query)); - return PostFilterAndSort(items, parent, null, query); - } - - var list = new List<BaseItem>(); - - list.Add(await GetUserView(SpecialFolder.LatestGames, "0", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.RecentlyPlayedGames, "1", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.GameFavorites, "2", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.GameSystems, "3", parent).ConfigureAwait(false)); - list.Add(await GetUserView(SpecialFolder.GameGenres, "4", parent).ConfigureAwait(false)); - - return GetResult(list, parent, query); - } - - private QueryResult<BaseItem> GetLatestGames(Folder parent, User user, InternalItemsQuery query) - { - query.SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }; - query.SortOrder = SortOrder.Descending; - - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }, i => i is Game && FilterItem(i, query)); - - return PostFilterAndSort(items, parent, GetSpecialItemsLimit(), query); - } - - private QueryResult<BaseItem> GetRecentlyPlayedGames(Folder parent, User user, InternalItemsQuery query) - { - query.IsPlayed = true; - query.SortBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.SortName }; - query.SortOrder = SortOrder.Descending; - - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }, i => i is Game && FilterItem(i, query)); - - return PostFilterAndSort(items, parent, GetSpecialItemsLimit(), query); - } - - private QueryResult<BaseItem> GetFavoriteGames(Folder parent, User user, InternalItemsQuery query) - { - query.IsFavorite = true; - - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }, i => i is Game && FilterItem(i, query)); - return PostFilterAndSort(items, parent, null, query); - } - private QueryResult<BaseItem> GetTvLatest(Folder parent, User user, InternalItemsQuery query) { query.SortBy = new[] { ItemSortBy.DateCreated, ItemSortBy.SortName }; @@ -745,49 +668,6 @@ namespace MediaBrowser.Controller.Entities return GetResult(items, queryParent, query); } - private QueryResult<BaseItem> GetGameSystems(Folder parent, User user, InternalItemsQuery query) - { - var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }, i => i is GameSystem && FilterItem(i, query)); - - return PostFilterAndSort(items, parent, null, query); - } - - private async Task<QueryResult<BaseItem>> GetGameGenreItems(Folder queryParent, Folder displayParent, User user, InternalItemsQuery query) - { - var items = GetRecursiveChildren(queryParent, user, new[] { CollectionType.Games }, - i => i is Game && i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase)); - - return GetResult(items, queryParent, query); - } - - private async Task<QueryResult<BaseItem>> GetGameGenres(Folder parent, User user, InternalItemsQuery query) - { - var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games }) - .OfType<Game>() - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(i => - { - try - { - return _libraryManager.GetGameGenre(i); - } - catch - { - // Full exception logged at lower levels - _logger.Error("Error getting game genre"); - return null; - } - - }) - .Where(i => i != null) - .Select(i => GetUserView(i.Name, SpecialFolder.GameGenre, i.SortName, parent)); - - var genres = await Task.WhenAll(tasks).ConfigureAwait(false); - - return GetResult(genres, parent, query); - } - private QueryResult<BaseItem> GetResult<T>(QueryResult<T> result) where T : BaseItem { @@ -1061,6 +941,11 @@ namespace MediaBrowser.Controller.Entities return false; } + if (request.GenreIds.Length > 0) + { + return false; + } + if (request.VideoTypes.Length > 0) { return false; @@ -1101,10 +986,15 @@ namespace MediaBrowser.Controller.Entities return false; } + if (request.MinIndexNumber.HasValue) + { + return false; + } + return true; } - public static IEnumerable<BaseItem> FilterVirtualEpisodes( + private static IEnumerable<BaseItem> FilterVirtualEpisodes( IEnumerable<BaseItem> items, bool? isMissing, bool? isVirtualUnaired, @@ -1374,7 +1264,7 @@ namespace MediaBrowser.Controller.Entities if (query.IsInBoxSet.HasValue) { var val = query.IsInBoxSet.Value; - if (item.Parents.OfType<BoxSet>().Any() != val) + if (item.GetParents().OfType<BoxSet>().Any() != val) { return false; } @@ -1657,6 +1547,16 @@ namespace MediaBrowser.Controller.Entities return false; } + // Apply genre filter + if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id => + { + var genreItem = libraryManager.GetItemById(id); + return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); + })) + { + return false; + } + // Apply year filter if (query.Years.Length > 0) { @@ -1779,6 +1679,16 @@ namespace MediaBrowser.Controller.Entities } } + if (query.MinIndexNumber.HasValue) + { + var val = query.MinIndexNumber.Value; + + if (!(item.IndexNumber.HasValue && item.IndexNumber.Value >= val)) + { + return false; + } + } + return true; } @@ -1789,12 +1699,12 @@ namespace MediaBrowser.Controller.Entities return _libraryManager.RootFolder .Children .OfType<Folder>() - .Where(i => !UserView.IsExcludedFromGrouping(i)); + .Where(UserView.IsEligibleForGrouping); } return user.RootFolder - .GetChildren(user, true, true) + .GetChildren(user, true) .OfType<Folder>() - .Where(i => user.IsFolderGrouped(i.Id) && !UserView.IsExcludedFromGrouping(i)); + .Where(i => user.IsFolderGrouped(i.Id) && UserView.IsEligibleForGrouping(i)); } private IEnumerable<Folder> GetMediaFolders(User user, IEnumerable<string> viewTypes) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 10068eed7..197222669 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -186,12 +186,6 @@ namespace MediaBrowser.Controller.Entities public string ShortcutPath { get; set; } /// <summary> - /// Gets or sets the tags. - /// </summary> - /// <value>The tags.</value> - public List<string> Tags { get; set; } - - /// <summary> /// Gets or sets the video bit rate. /// </summary> /// <value>The video bit rate.</value> @@ -356,7 +350,7 @@ namespace MediaBrowser.Controller.Entities // Must have a parent to have additional parts or alternate versions // In other words, it must be part of the Parent/Child tree // The additional parts won't have additional parts themselves - if (LocationType == LocationType.FileSystem && Parent != null) + if (LocationType == LocationType.FileSystem && GetParent() != null) { if (!IsStacked) { diff --git a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs index ff22b0cdc..daa670d83 100644 --- a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs +++ b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs @@ -67,5 +67,19 @@ namespace MediaBrowser.Controller.FileOrganization /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken); + + /// <summary> + /// Returns a list of smart match entries + /// </summary> + /// <param name="query">The query.</param> + /// <returns>IEnumerable{SmartMatchInfo}.</returns> + QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query); + + /// <summary> + /// Deletes a smart match entry. + /// </summary> + /// <param name="ItemName">Item name.</param> + /// <param name="matchString">The match string to delete.</param> + void DeleteSmartMatchEntry(string ItemName, string matchString); } } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 2af53efa9..1c515edd5 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -329,7 +329,6 @@ namespace MediaBrowser.Controller.Library /// <param name="parentId">The parent identifier.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - /// <param name="uniqueId">The unique identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<UserView>.</returns> Task<UserView> GetNamedView(User user, @@ -337,7 +336,6 @@ namespace MediaBrowser.Controller.Library string parentId, string viewType, string sortName, - string uniqueId, CancellationToken cancellationToken); /// <summary> @@ -391,13 +389,11 @@ namespace MediaBrowser.Controller.Library /// <param name="parent">The parent.</param> /// <param name="viewType">Type of the view.</param> /// <param name="sortName">Name of the sort.</param> - /// <param name="uniqueId">The unique identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<UserView>.</returns> Task<UserView> GetShadowView(BaseItem parent, string viewType, string sortName, - string uniqueId, CancellationToken cancellationToken); /// <summary> @@ -543,5 +539,29 @@ namespace MediaBrowser.Controller.Library /// <param name="imageIndex">Index of the image.</param> /// <returns>Task.</returns> Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex); + + /// <summary> + /// Gets the items. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="parentIds">The parent ids.</param> + /// <returns>List<BaseItem>.</returns> + IEnumerable<BaseItem> GetItems(InternalItemsQuery query, IEnumerable<string> parentIds); + + /// <summary> + /// Gets the items result. + /// </summary> + /// <param name="query">The query.</param> + /// <param name="parentIds">The parent ids.</param> + /// <returns>QueryResult<BaseItem>.</returns> + QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query, IEnumerable<string> parentIds); + + /// <summary> + /// Ignores the file. + /// </summary> + /// <param name="file">The file.</param> + /// <param name="parent">The parent.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> + bool IgnoreFile(FileSystemMetadata file, BaseItem parent); } }
\ No newline at end of file diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index 1c0415ca1..9084ea56b 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -155,7 +155,7 @@ namespace MediaBrowser.Controller.Library // Not officially supported but in some cases we can handle it. if (item == null) { - item = parent.Parents.OfType<T>().FirstOrDefault(); + item = parent.GetParents().OfType<T>().FirstOrDefault(); } return item != null; diff --git a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs index dd1c9c07a..4c9831bf9 100644 --- a/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs +++ b/MediaBrowser.Controller/Library/LibraryManagerExtensions.cs @@ -6,14 +6,6 @@ namespace MediaBrowser.Controller.Library { public static class LibraryManagerExtensions { - public static Task DeleteItem(this ILibraryManager manager, BaseItem item) - { - return manager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = true - }); - } - public static BaseItem GetItemById(this ILibraryManager manager, string id) { return manager.GetItemById(new Guid(id)); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 241b01405..05c7448c3 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -45,6 +45,13 @@ namespace MediaBrowser.Controller.LiveTv Task DeleteRecording(string id); /// <summary> + /// Deletes the recording. + /// </summary> + /// <param name="recording">The recording.</param> + /// <returns>Task.</returns> + Task DeleteRecording(ILiveTvRecording recording); + + /// <summary> /// Cancels the timer. /// </summary> /// <param name="id">The identifier.</param> @@ -338,9 +345,9 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <param name="item">The item.</param> /// <param name="dto">The dto.</param> - /// <param name="addChannelInfo">if set to <c>true</c> [add channel information].</param> + /// <param name="fields">The fields.</param> /// <param name="user">The user.</param> - void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, bool addChannelInfo, User user = null); + void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, List<ItemFields> fields, User user = null); /// <summary> /// Saves the tuner host. /// </summary> diff --git a/MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs b/MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs index 03c05ec69..c3d843f85 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvAudioRecording.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.LiveTv { @@ -36,7 +38,6 @@ namespace MediaBrowser.Controller.LiveTv public bool IsLive { get; set; } [IgnoreDataMember] public bool IsPremiere { get; set; } - public ProgramAudio? Audio { get; set; } /// <summary> /// Gets the user data key. @@ -106,9 +107,9 @@ namespace MediaBrowser.Controller.LiveTv } } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram); + return UnratedItem.LiveTvProgram; } protected override string GetInternalMetadataPath(string basePath) @@ -140,5 +141,15 @@ namespace MediaBrowser.Controller.LiveTv return list; } + + public override bool IsVisibleStandalone(User user) + { + return IsVisible(user); + } + + public override Task Delete(DeleteOptions options) + { + return LiveTvManager.DeleteRecording(this); + } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 89168c578..8c4ee92cd 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -22,9 +22,9 @@ namespace MediaBrowser.Controller.LiveTv return GetClientTypeName() + "-" + Name; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.LiveTvChannel); + return UnratedItem.LiveTvChannel; } /// <summary> diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 88bbe19b7..fcd065e79 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -40,10 +40,11 @@ namespace MediaBrowser.Controller.LiveTv } /// <summary> - /// Gets or sets the type of the channel. + /// Gets or sets the name. /// </summary> - /// <value>The type of the channel.</value> - public ChannelType ChannelType { get; set; } + /// <value>The name.</value> + [IgnoreDataMember] + public string ServiceName { get; set; } /// <summary> /// The start date of the program, in UTC. @@ -52,12 +53,6 @@ namespace MediaBrowser.Controller.LiveTv public DateTime StartDate { get; set; } /// <summary> - /// Gets or sets the audio. - /// </summary> - /// <value>The audio.</value> - public ProgramAudio? Audio { get; set; } - - /// <summary> /// Gets or sets a value indicating whether this instance is repeat. /// </summary> /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> @@ -72,12 +67,6 @@ namespace MediaBrowser.Controller.LiveTv public string EpisodeTitle { get; set; } /// <summary> - /// Gets or sets the name of the service. - /// </summary> - /// <value>The name of the service.</value> - public string ServiceName { get; set; } - - /// <summary> /// Gets or sets a value indicating whether this instance is movie. /// </summary> /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> @@ -153,14 +142,14 @@ namespace MediaBrowser.Controller.LiveTv } } - [IgnoreDataMember] - public override string MediaType - { - get - { - return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; - } - } + //[IgnoreDataMember] + //public override string MediaType + //{ + // get + // { + // return ChannelType == ChannelType.TV ? Model.Entities.MediaType.Video : Model.Entities.MediaType.Audio; + // } + //} [IgnoreDataMember] public bool IsAiring @@ -189,9 +178,9 @@ namespace MediaBrowser.Controller.LiveTv return "Program"; } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram); + return UnratedItem.LiveTvProgram; } protected override string GetInternalMetadataPath(string basePath) @@ -236,5 +225,14 @@ namespace MediaBrowser.Controller.LiveTv return base.SupportsPeople; } } + + [IgnoreDataMember] + public override bool SupportsAncestors + { + get + { + return false; + } + } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs b/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs index 27594317d..5492a29f3 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvVideoRecording.cs @@ -9,6 +9,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.LiveTv { @@ -36,7 +38,6 @@ namespace MediaBrowser.Controller.LiveTv public bool IsLive { get; set; } [IgnoreDataMember] public bool IsPremiere { get; set; } - public ProgramAudio? Audio { get; set; } /// <summary> /// Gets the user data key. @@ -121,9 +122,9 @@ namespace MediaBrowser.Controller.LiveTv } } - protected override bool GetBlockUnratedValue(UserPolicy config) + public override UnratedItem GetBlockUnratedType() { - return config.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram); + return UnratedItem.LiveTvProgram; } protected override string GetInternalMetadataPath(string basePath) @@ -155,5 +156,15 @@ namespace MediaBrowser.Controller.LiveTv return list; } + + public override bool IsVisibleStandalone(User user) + { + return IsVisible(user); + } + + public override Task Delete(DeleteOptions options) + { + return LiveTvManager.DeleteRecording(this); + } } } diff --git a/MediaBrowser.Controller/LiveTv/RecordingGroup.cs b/MediaBrowser.Controller/LiveTv/RecordingGroup.cs index 175cf162b..2d58ef67f 100644 --- a/MediaBrowser.Controller/LiveTv/RecordingGroup.cs +++ b/MediaBrowser.Controller/LiveTv/RecordingGroup.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.LiveTv @@ -11,6 +12,11 @@ namespace MediaBrowser.Controller.LiveTv return false; } + public override UnratedItem GetBlockUnratedType() + { + return UnratedItem.LiveTvProgram; + } + public override bool SupportsLocalMetadata { get diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 471aa38d4..9c17c8d9b 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -162,6 +162,7 @@ <Compile Include="Entities\IHasThemeMedia.cs" /> <Compile Include="Entities\IHasTrailers.cs" /> <Compile Include="Entities\IHasUserData.cs" /> + <Compile Include="Entities\IHiddenFromDisplay.cs" /> <Compile Include="Entities\IItemByName.cs" /> <Compile Include="Entities\ILibraryItem.cs" /> <Compile Include="Entities\ImageSourceInfo.cs" /> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 8fc0aedd3..533d66b95 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -176,6 +176,20 @@ namespace MediaBrowser.Controller.Persistence /// <param name="query">The query.</param> /// <returns>QueryResult<Tuple<Guid, System.String>>.</returns> QueryResult<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query); + + /// <summary> + /// Gets the item list. + /// </summary> + /// <param name="query">The query.</param> + /// <returns>List<BaseItem>.</returns> + IEnumerable<BaseItem> GetItemList(InternalItemsQuery query); + + /// <summary> + /// Updates the inherited values. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + Task UpdateInheritedValues(CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index fdc36db35..0f9af6550 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -144,6 +144,7 @@ namespace MediaBrowser.Controller.Playlists public string PlaylistMediaType { get; set; } + [IgnoreDataMember] public override string MediaType { get diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs index 3e5353812..f9060d184 100644 --- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs +++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs @@ -889,6 +889,10 @@ namespace MediaBrowser.Controller.Providers { video.Video3DFormat = Video3DFormat.FullSideBySide; } + else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } } break; } diff --git a/MediaBrowser.Controller/Providers/MetadataStatus.cs b/MediaBrowser.Controller/Providers/MetadataStatus.cs index 9b946aee2..b19377a68 100644 --- a/MediaBrowser.Controller/Providers/MetadataStatus.cs +++ b/MediaBrowser.Controller/Providers/MetadataStatus.cs @@ -11,24 +11,6 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// Gets or sets the name of the item. - /// </summary> - /// <value>The name of the item.</value> - public string ItemName { get; set; } - - /// <summary> - /// Gets or sets the type of the item. - /// </summary> - /// <value>The type of the item.</value> - public string ItemType { get; set; } - - /// <summary> - /// Gets or sets the name of the series. - /// </summary> - /// <value>The name of the series.</value> - public string SeriesName { get; set; } - - /// <summary> /// Gets or sets the date last metadata refresh. /// </summary> /// <value>The date last metadata refresh.</value> @@ -40,22 +22,8 @@ namespace MediaBrowser.Controller.Providers /// <value>The date last images refresh.</value> public DateTime? DateLastImagesRefresh { get; set; } - /// <summary> - /// Gets or sets the last result error message. - /// </summary> - /// <value>The last result error message.</value> - public string LastErrorMessage { get; set; } - public DateTime? ItemDateModified { get; set; } - public void AddStatus(string errorMessage) - { - if (string.IsNullOrEmpty(LastErrorMessage)) - { - LastErrorMessage = errorMessage; - } - } - public bool IsDirty { get; private set; } public void SetDateLastMetadataRefresh(DateTime? date) diff --git a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs index e85535548..2c82882c6 100644 --- a/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs +++ b/MediaBrowser.Controller/Resolvers/IResolverIgnoreRule.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Library; +using CommonIO; +using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Resolvers { @@ -7,6 +8,6 @@ namespace MediaBrowser.Controller.Resolvers /// </summary> public interface IResolverIgnoreRule { - bool ShouldIgnore(ItemResolveArgs args); + bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent); } } diff --git a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs b/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs index 2ab27fde5..21289970e 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly ILocalizationManager _localization; private readonly IChannelManager _channelManager; private readonly IMediaSourceManager _mediaSourceManager; + private readonly IUserViewManager _userViewManager; public ContentDirectory(IDlnaManager dlna, IUserDataManager userDataManager, @@ -34,7 +35,7 @@ namespace MediaBrowser.Dlna.ContentDirectory IServerConfigurationManager config, IUserManager userManager, ILogger logger, - IHttpClient httpClient, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager) + IHttpClient httpClient, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager) : base(logger, httpClient) { _dlna = dlna; @@ -46,6 +47,7 @@ namespace MediaBrowser.Dlna.ContentDirectory _localization = localization; _channelManager = channelManager; _mediaSourceManager = mediaSourceManager; + _userViewManager = userViewManager; } private int SystemUpdateId @@ -86,7 +88,8 @@ namespace MediaBrowser.Dlna.ContentDirectory _config, _localization, _channelManager, - _mediaSourceManager) + _mediaSourceManager, + _userViewManager) .ProcessControlRequest(request); } diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs index a9ce5587d..fb7596387 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs @@ -24,6 +24,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; +using MediaBrowser.Model.Library; namespace MediaBrowser.Dlna.ContentDirectory { @@ -34,6 +35,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly IUserDataManager _userDataManager; private readonly IServerConfigurationManager _config; private readonly User _user; + private readonly IUserViewManager _userViewManager; private const string NS_DC = "http://purl.org/dc/elements/1.1/"; private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; @@ -47,7 +49,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly DeviceProfile _profile; - public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager) + public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager) : base(config, logger) { _libraryManager = libraryManager; @@ -55,6 +57,7 @@ namespace MediaBrowser.Dlna.ContentDirectory _user = user; _systemUpdateId = systemUpdateId; _channelManager = channelManager; + _userViewManager = userViewManager; _profile = profile; _config = config; @@ -450,16 +453,32 @@ namespace MediaBrowser.Dlna.ContentDirectory sortOrders.Add(ItemSortBy.SortName); } - var queryResult = await folder.GetItems(new InternalItemsQuery + QueryResult<BaseItem> queryResult; + + if (folder is UserRootFolder) { - Limit = limit, - StartIndex = startIndex, - SortBy = sortOrders.ToArray(), - SortOrder = sort.SortOrder, - User = user, - Filter = FilterUnsupportedContent + var views = await _userViewManager.GetUserViews(new UserViewQuery { UserId = user.Id.ToString("N"), PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows, CollectionType.Music } }, CancellationToken.None) + .ConfigureAwait(false); + + queryResult = new QueryResult<BaseItem> + { + Items = views.Cast<BaseItem>().ToArray() + }; + queryResult.TotalRecordCount = queryResult.Items.Length; + } + else + { + queryResult = await folder.GetItems(new InternalItemsQuery + { + Limit = limit, + StartIndex = startIndex, + SortBy = sortOrders.ToArray(), + SortOrder = sort.SortOrder, + User = user, + Filter = FilterUnsupportedContent - }).ConfigureAwait(false); + }).ConfigureAwait(false); + } var options = _config.GetDlnaConfiguration(); @@ -481,23 +500,17 @@ namespace MediaBrowser.Dlna.ContentDirectory private QueryResult<ServerItem> GetItemsFromPerson(Person person, User user, int? startIndex, int? limit) { - var itemsWithPerson = _libraryManager.GetItems(new InternalItemsQuery + var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - Person = person.Name - - }).Items; - - var items = itemsWithPerson - .Where(i => i is Movie || i is Series || i is IChannelItem) - .Where(i => i.IsVisibleStandalone(user)) - .ToList(); + Person = person.Name, + IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name, typeof(ChannelVideoItem).Name }, + SortBy = new[] { ItemSortBy.SortName }, + Limit = limit, + StartIndex = startIndex - items = _libraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) - .Skip(startIndex ?? 0) - .Take(limit ?? int.MaxValue) - .ToList(); + }, new string[] { }); - var serverItems = items.Select(i => new ServerItem + var serverItems = itemsResult.Items.Select(i => new ServerItem { Item = i, StubType = null @@ -506,7 +519,7 @@ namespace MediaBrowser.Dlna.ContentDirectory return new QueryResult<ServerItem> { - TotalRecordCount = serverItems.Length, + TotalRecordCount = itemsResult.TotalRecordCount, Items = serverItems }; } diff --git a/MediaBrowser.Dlna/Didl/DidlBuilder.cs b/MediaBrowser.Dlna/Didl/DidlBuilder.cs index 0a5b591ec..40507db0c 100644 --- a/MediaBrowser.Dlna/Didl/DidlBuilder.cs +++ b/MediaBrowser.Dlna/Didl/DidlBuilder.cs @@ -966,7 +966,7 @@ namespace MediaBrowser.Dlna.Didl } } - item = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary)); + item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); if (item != null) { diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index 2a688fa54..63a36744f 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -210,6 +210,10 @@ namespace MediaBrowser.Dlna throw new ArgumentNullException("headers"); } + //_logger.Debug("GetProfile. Headers: " + _jsonSerializer.SerializeToString(headers)); + // Convert to case insensitive + headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase); + var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification)); if (profile != null) @@ -221,7 +225,7 @@ namespace MediaBrowser.Dlna string userAgent = null; headers.TryGetValue("User-Agent", out userAgent); - var msg = "No matching device profile found. The default will be used. "; + var msg = "No matching device profile via headers found. The default will be used. "; if (!string.IsNullOrEmpty(userAgent)) { msg += "User-agent: " + userAgent + ". "; @@ -249,7 +253,9 @@ namespace MediaBrowser.Dlna case HeaderMatchType.Equals: return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase); case HeaderMatchType.Substring: - return value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; + var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1; + //_logger.Debug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch); + return isMatch; case HeaderMatchType.Regex: // Reports of IgnoreCase not working on linux so try it a couple different ways. return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase) || Regex.IsMatch(value.ToUpper(), header.Value.ToUpper(), RegexOptions.IgnoreCase); diff --git a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs b/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs index ab6674afc..ff4223c72 100644 --- a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs +++ b/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs @@ -114,7 +114,7 @@ namespace MediaBrowser.Dlna.PlayTo { Url = url, UserAgent = USERAGENT, - LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLogging, + LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog, LogErrorResponseBody = true }; diff --git a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs b/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs index 386370596..281dee3e0 100644 --- a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs +++ b/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs @@ -122,7 +122,7 @@ namespace MediaBrowser.Dlna.Server builder.Append("<serialNumber>" + SecurityElement.Escape(_profile.SerialNumber) + "</serialNumber>"); } - builder.Append("<UDN>uuid:" + SecurityElement.Escape(_serverUdn) + "</UDN>"); + builder.Append("<UDN>uuid:" + SecurityElement.Escape(_serverId) + "</UDN>"); builder.Append("<presentationURL>" + SecurityElement.Escape(_serverAddress) + "</presentationURL>"); if (!EnableAbsoluteUrls) diff --git a/MediaBrowser.Dlna/Service/BaseControlHandler.cs b/MediaBrowser.Dlna/Service/BaseControlHandler.cs index a65520d65..c5de76eb5 100644 --- a/MediaBrowser.Dlna/Service/BaseControlHandler.cs +++ b/MediaBrowser.Dlna/Service/BaseControlHandler.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Dlna.Service { try { - var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLogging; + var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog; if (enableDebugLogging) { diff --git a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs index ed3698e43..4f7dffdd9 100644 --- a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs +++ b/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Dlna.Ssdp return; } - if (_config.GetDlnaConfiguration().EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLog) { var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); var headerText = string.Join(",", headerTexts.ToArray()); diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index f800a12c1..82fc5e466 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.Dlna.Ssdp { public class SsdpHandler : IDisposable, ISsdpHandler { - private Socket _socket; + private Socket _multicastSocket; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; @@ -40,6 +40,9 @@ namespace MediaBrowser.Dlna.Ssdp private readonly IApplicationHost _appHost; + private readonly int _unicastPort = 1901; + private UdpClient _unicastClient; + public SsdpHandler(ILogger logger, IServerConfigurationManager config, IApplicationHost appHost) { _logger = logger; @@ -92,7 +95,7 @@ namespace MediaBrowser.Dlna.Ssdp { TimeSpan delay = GetSearchDelay(headers); - if (_config.GetDlnaConfiguration().EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLog) { _logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds); } @@ -123,6 +126,8 @@ namespace MediaBrowser.Dlna.Ssdp RestartSocketListener(); ReloadAliveNotifier(); + //CreateUnicastClient(); + SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -150,26 +155,29 @@ namespace MediaBrowser.Dlna.Ssdp // Seconds to delay response values["MX"] = "3"; + var header = "M-SEARCH * HTTP/1.1"; + + var msg = new SsdpMessageBuilder().BuildMessage(header, values); + // UDP is unreliable, so send 3 requests at a time (per Upnp spec, sec 1.1.2) - SendDatagram("M-SEARCH * HTTP/1.1", values, _ssdpEndp, localIp, true, 2); + SendDatagram(msg, _ssdpEndp, localIp, true); + + //SendUnicastRequest(msg); } - public async void SendDatagram(string header, - Dictionary<string, string> values, + public async void SendDatagram(string msg, EndPoint endpoint, EndPoint localAddress, bool isBroadcast, - int sendCount) + int sendCount = 3) { - var msg = new SsdpMessageBuilder().BuildMessage(header, values); - - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLogging; + var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; for (var i = 0; i < sendCount; i++) { if (i > 0) { - await Task.Delay(500).ConfigureAwait(false); + await Task.Delay(200).ConfigureAwait(false); } var dgram = new Datagram(endpoint, localAddress, _logger, msg, isBroadcast, enableDebugLogging); @@ -202,7 +210,7 @@ namespace MediaBrowser.Dlna.Ssdp private void RespondToSearch(EndPoint endpoint, string deviceType) { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLogging; + var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; var isLogged = false; @@ -232,8 +240,10 @@ namespace MediaBrowser.Dlna.Ssdp values["ST"] = d.Type; values["USN"] = d.USN; - SendDatagram(header, values, endpoint, null, false, 1); - SendDatagram(header, values, endpoint, new IPEndPoint(d.Address, 0), false, 1); + var msg = new SsdpMessageBuilder().BuildMessage(header, values); + + SendDatagram(msg, endpoint, null, false, 1); + SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 1); //SendDatagram(header, values, endpoint, null, true); if (enableDebugLogging) @@ -253,7 +263,7 @@ namespace MediaBrowser.Dlna.Ssdp try { - _socket = CreateMulticastSocket(); + _multicastSocket = CreateMulticastSocket(); _logger.Info("MultiCast socket created"); @@ -274,8 +284,7 @@ namespace MediaBrowser.Dlna.Ssdp EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - _socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, - buffer); + _multicastSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpoint, ReceiveCallback, buffer); } catch (ObjectDisposedException) { @@ -301,11 +310,11 @@ namespace MediaBrowser.Dlna.Ssdp { EndPoint endpoint = new IPEndPoint(IPAddress.Any, SSDPPort); - var length = _socket.EndReceiveFrom(result, ref endpoint); + var length = _multicastSocket.EndReceiveFrom(result, ref endpoint); var received = (byte[])result.AsyncState; - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLogging; + var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; if (enableDebugLogging) { @@ -325,7 +334,7 @@ namespace MediaBrowser.Dlna.Ssdp var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); var headerText = string.Join(",", headerTexts.ToArray()); - _logger.Debug("{0} message received from {1} on {3}. Headers: {2}", args.Method, args.EndPoint, headerText, _socket.LocalEndPoint); + _logger.Debug("{0} message received from {1} on {3}. Headers: {2}", args.Method, args.EndPoint, headerText, _multicastSocket.LocalEndPoint); } OnMessageReceived(args); @@ -342,7 +351,7 @@ namespace MediaBrowser.Dlna.Ssdp _logger.ErrorException("Failed to read SSDP message", ex); } - if (_socket != null) + if (_multicastSocket != null) { Receive(); } @@ -375,17 +384,18 @@ namespace MediaBrowser.Dlna.Ssdp _isDisposed = true; + DisposeUnicastClient(); DisposeSocket(); StopAliveNotifier(); } private void DisposeSocket() { - if (_socket != null) + if (_multicastSocket != null) { - _socket.Close(); - _socket.Dispose(); - _socket = null; + _multicastSocket.Close(); + _multicastSocket.Dispose(); + _multicastSocket = null; } } @@ -404,7 +414,7 @@ namespace MediaBrowser.Dlna.Ssdp private void NotifyAll() { - var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLogging; + var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog; if (enableDebugLogging) { @@ -412,11 +422,11 @@ namespace MediaBrowser.Dlna.Ssdp } foreach (var d in RegisteredDevices) { - NotifyDevice(d, "alive", 1, enableDebugLogging); + NotifyDevice(d, "alive", enableDebugLogging); } } - private void NotifyDevice(UpnpDevice dev, string type, int sendCount, bool logMessage) + private void NotifyDevice(UpnpDevice dev, string type, bool logMessage) { const string header = "NOTIFY * HTTP/1.1"; @@ -436,7 +446,9 @@ namespace MediaBrowser.Dlna.Ssdp _logger.Debug("{0} said {1}", dev.USN, type); } - SendDatagram(header, values, _ssdpEndp, new IPEndPoint(dev.Address, 0), true, sendCount); + var msg = new SsdpMessageBuilder().BuildMessage(header, values); + + SendDatagram(msg, _ssdpEndp, new IPEndPoint(dev.Address, 0), true); } public void RegisterNotification(Guid uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services) @@ -457,13 +469,111 @@ namespace MediaBrowser.Dlna.Ssdp foreach (var d in dl.ToList()) { - NotifyDevice(d, "byebye", 2, true); + NotifyDevice(d, "byebye", true); } _logger.Debug("Unregistered mount {0}", uuid); } } + private void CreateUnicastClient() + { + if (_unicastClient == null) + { + try + { + _unicastClient = new UdpClient(_unicastPort); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating unicast client", ex); + } + + try + { + UnicastSetBeginReceive(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in UnicastSetBeginReceive", ex); + } + } + } + + private void DisposeUnicastClient() + { + if (_unicastClient != null) + { + try + { + _unicastClient.Close(); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing unicast client", ex); + } + + _unicastClient = null; + } + } + + /// <summary> + /// Listen for Unicast SSDP Responses + /// </summary> + private void UnicastSetBeginReceive() + { + var ipRxEnd = new IPEndPoint(IPAddress.Any, _unicastPort); + var udpListener = new UdpState { EndPoint = ipRxEnd }; + + udpListener.UdpClient = _unicastClient; + _unicastClient.BeginReceive(UnicastReceiveCallback, udpListener); + } + + /// <summary> + /// The UnicastReceiveCallback receives Http Responses + /// and Fired the SatIpDeviceFound Event for adding the SatIpDevice + /// </summary> + /// <param name="ar"></param> + private void UnicastReceiveCallback(IAsyncResult ar) + { + var udpClient = ((UdpState)(ar.AsyncState)).UdpClient; + var endpoint = ((UdpState)(ar.AsyncState)).EndPoint; + if (udpClient.Client != null) + { + var responseBytes = udpClient.EndReceive(ar, ref endpoint); + var args = SsdpHelper.ParseSsdpResponse(responseBytes); + + args.EndPoint = endpoint; + + OnMessageReceived(args); + + UnicastSetBeginReceive(); + } + } + + private async void SendUnicastRequest(string request) + { + if (_unicastClient == null) + { + return; + } + + _logger.Debug("Sending unicast search request"); + + byte[] req = Encoding.ASCII.GetBytes(request); + var ipSsdp = IPAddress.Parse(SSDPAddr); + var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort); + + for (var i = 0; i < 3; i++) + { + if (i > 0) + { + await Task.Delay(50).ConfigureAwait(false); + } + _unicastClient.Send(req, req.Length, ipTxEnd); + } + } + private readonly object _notificationTimerSyncLock = new object(); private int _aliveNotifierIntervalMs; private void ReloadAliveNotifier() @@ -511,5 +621,11 @@ namespace MediaBrowser.Dlna.Ssdp } } } + + public class UdpState + { + public UdpClient UdpClient; + public IPEndPoint EndPoint; + } } } diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index c6f81d874..0bbe3f38a 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -176,7 +176,7 @@ namespace MediaBrowser.LocalMetadata.Images "default" }; - if (item is MusicAlbum || item is MusicArtist) + if (item is MusicAlbum || item is MusicArtist || item is Photo) { // these prefer folder names.Insert(0, "poster"); diff --git a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs index c9a4cb537..48c4fe2fd 100644 --- a/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs +++ b/MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs @@ -736,6 +736,9 @@ namespace MediaBrowser.LocalMetadata.Savers case Video3DFormat.HalfTopAndBottom: builder.Append("<Format3D>HTAB</Format3D>"); break; + case Video3DFormat.MVC: + builder.Append("<Format3D>MVC</Format3D>"); + break; } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs index b488741d1..a4d4797eb 100644 --- a/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/AudioEncoder.cs @@ -19,38 +19,40 @@ namespace MediaBrowser.MediaEncoding.Encoder { } - protected override string GetCommandLineArguments(EncodingJob job) + protected override string GetCommandLineArguments(EncodingJob state) { var audioTranscodeParams = new List<string>(); - var bitrate = job.OutputAudioBitrate; + var bitrate = state.OutputAudioBitrate; if (bitrate.HasValue) { audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(UsCulture)); } - if (job.OutputAudioChannels.HasValue) + if (state.OutputAudioChannels.HasValue) { - audioTranscodeParams.Add("-ac " + job.OutputAudioChannels.Value.ToString(UsCulture)); + audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture)); } - if (job.OutputAudioSampleRate.HasValue) + if (state.OutputAudioSampleRate.HasValue) { - audioTranscodeParams.Add("-ar " + job.OutputAudioSampleRate.Value.ToString(UsCulture)); + audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture)); } - var threads = GetNumberOfThreads(job, false); + const string vn = " -vn"; - var inputModifier = GetInputModifier(job); + var threads = GetNumberOfThreads(state, false); + + var inputModifier = GetInputModifier(state); return string.Format("{0} {1} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1 -y \"{5}\"", inputModifier, - GetInputArgument(job), + GetInputArgument(state), threads, - " -vn", + vn, string.Join(" ", audioTranscodeParams.ToArray()), - job.OutputFilePath).Trim(); + state.OutputFilePath).Trim(); } protected override string GetOutputFileExtension(EncodingJob state) diff --git a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs index 98e4a58a6..4fabed850 100644 --- a/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/BaseEncoder.cs @@ -303,15 +303,15 @@ namespace MediaBrowser.MediaEncoding.Encoder return job.Options.CpuCoreLimit ?? 0; } - protected string GetInputModifier(EncodingJob job, bool genPts = true) + protected string GetInputModifier(EncodingJob state, bool genPts = true) { var inputModifier = string.Empty; - var probeSize = GetProbeSizeArgument(job); + var probeSize = GetProbeSizeArgument(state); inputModifier += " " + probeSize; inputModifier = inputModifier.Trim(); - var userAgentParam = GetUserAgentParam(job); + var userAgentParam = GetUserAgentParam(state); if (!string.IsNullOrWhiteSpace(userAgentParam)) { @@ -320,35 +320,43 @@ namespace MediaBrowser.MediaEncoding.Encoder inputModifier = inputModifier.Trim(); - inputModifier += " " + GetFastSeekCommandLineParameter(job.Options); + inputModifier += " " + GetFastSeekCommandLineParameter(state.Options); inputModifier = inputModifier.Trim(); - if (job.IsVideoRequest && genPts) + if (state.IsVideoRequest && genPts) { inputModifier += " -fflags +genpts"; } - if (!string.IsNullOrEmpty(job.InputAudioSync)) + if (!string.IsNullOrEmpty(state.InputAudioSync)) { - inputModifier += " -async " + job.InputAudioSync; + inputModifier += " -async " + state.InputAudioSync; } - if (!string.IsNullOrEmpty(job.InputVideoSync)) + if (!string.IsNullOrEmpty(state.InputVideoSync)) { - inputModifier += " -vsync " + job.InputVideoSync; + inputModifier += " -vsync " + state.InputVideoSync; } - if (job.ReadInputAtNativeFramerate) + if (state.ReadInputAtNativeFramerate) { inputModifier += " -re"; } - var videoDecoder = GetVideoDecoder(job); + var videoDecoder = GetVideoDecoder(state); if (!string.IsNullOrWhiteSpace(videoDecoder)) { inputModifier += " " + videoDecoder; } + //if (state.IsVideoRequest) + //{ + // if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase)) + // { + // //inputModifier += " -noaccurate_seek"; + // } + //} + return inputModifier; } @@ -392,11 +400,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return null; } - private string GetUserAgentParam(EncodingJob job) + private string GetUserAgentParam(EncodingJob state) { string useragent = null; - job.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); + state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); if (!string.IsNullOrWhiteSpace(useragent)) { @@ -409,31 +417,31 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Gets the probe size argument. /// </summary> - /// <param name="job">The job.</param> + /// <param name="state">The state.</param> /// <returns>System.String.</returns> - private string GetProbeSizeArgument(EncodingJob job) + private string GetProbeSizeArgument(EncodingJob state) { - if (job.PlayableStreamFileNames.Count > 0) + if (state.PlayableStreamFileNames.Count > 0) { - return MediaEncoder.GetProbeSizeArgument(job.PlayableStreamFileNames.ToArray(), job.InputProtocol); + return MediaEncoder.GetProbeSizeArgument(state.PlayableStreamFileNames.ToArray(), state.InputProtocol); } - return MediaEncoder.GetProbeSizeArgument(new[] { job.MediaPath }, job.InputProtocol); + return MediaEncoder.GetProbeSizeArgument(new[] { state.MediaPath }, state.InputProtocol); } /// <summary> /// Gets the fast seek command line parameter. /// </summary> - /// <param name="options">The options.</param> + /// <param name="request">The request.</param> /// <returns>System.String.</returns> /// <value>The fast seek command line parameter.</value> - protected string GetFastSeekCommandLineParameter(EncodingJobOptions options) + protected string GetFastSeekCommandLineParameter(EncodingJobOptions request) { - var time = options.StartTimeTicks; + var time = request.StartTimeTicks ?? 0; - if (time.HasValue && time.Value > 0) + if (time > 0) { - return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value)); + return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time)); } return string.Empty; @@ -442,34 +450,35 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Gets the input argument. /// </summary> - /// <param name="job">The job.</param> + /// <param name="state">The state.</param> /// <returns>System.String.</returns> - protected string GetInputArgument(EncodingJob job) + protected string GetInputArgument(EncodingJob state) { - var arg = "-i " + GetInputPathArgument(job); + var arg = string.Format("-i {0}", GetInputPathArgument(state)); - if (job.SubtitleStream != null) + if (state.SubtitleStream != null) { - if (job.SubtitleStream.IsExternal && !job.SubtitleStream.IsTextSubtitleStream) + if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { - arg += " -i \"" + job.SubtitleStream.Path + "\""; + arg += " -i \"" + state.SubtitleStream.Path + "\""; } } - return arg; + return arg.Trim(); } - private string GetInputPathArgument(EncodingJob job) + private string GetInputPathArgument(EncodingJob state) { - var protocol = job.InputProtocol; + var protocol = state.InputProtocol; + var mediaPath = state.MediaPath ?? string.Empty; - var inputPath = new[] { job.MediaPath }; + var inputPath = new[] { mediaPath }; - if (job.IsInputVideo) + if (state.IsInputVideo) { - if (!(job.VideoType == VideoType.Iso && job.IsoMount == null)) + if (!(state.VideoType == VideoType.Iso && state.IsoMount == null)) { - inputPath = MediaEncoderHelpers.GetInputArgument(FileSystem, job.MediaPath, job.InputProtocol, job.IsoMount, job.PlayableStreamFileNames); + inputPath = MediaEncoderHelpers.GetInputArgument(FileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames); } } @@ -491,7 +500,7 @@ namespace MediaBrowser.MediaEncoding.Encoder }, false, cancellationToken).ConfigureAwait(false); - AttachMediaStreamInfo(state, liveStreamResponse.MediaSource, state.Options); + AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.Options); if (state.IsVideoRequest) { @@ -505,11 +514,11 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - private void AttachMediaStreamInfo(EncodingJob state, + private void AttachMediaSourceInfo(EncodingJob state, MediaSourceInfo mediaSource, EncodingJobOptions videoRequest) { - EncodingJobFactory.AttachMediaStreamInfo(state, mediaSource, videoRequest); + EncodingJobFactory.AttachMediaSourceInfo(state, mediaSource, videoRequest); } /// <summary> @@ -559,9 +568,8 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="state">The state.</param> /// <param name="videoCodec">The video codec.</param> - /// <param name="isHls">if set to <c>true</c> [is HLS].</param> /// <returns>System.String.</returns> - protected string GetVideoQualityParam(EncodingJob state, string videoCodec, bool isHls) + protected string GetVideoQualityParam(EncodingJob state, string videoCodec) { var param = string.Empty; @@ -572,7 +580,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { param = "-preset superfast"; - param += " -crf 28"; + param += " -crf 23"; } else if (string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) @@ -582,6 +590,19 @@ namespace MediaBrowser.MediaEncoding.Encoder param += " -crf 28"; } + // h264 (h264_qsv) + else if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + param = "-preset 7 -look_ahead 0"; + + } + + // h264 (libnvenc) + else if (string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase)) + { + param = "-preset high-performance"; + } + // webm else if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) { @@ -626,7 +647,7 @@ namespace MediaBrowser.MediaEncoding.Encoder param = "-mbd 2"; } - param += GetVideoBitrateParam(state, videoCodec, isHls); + param += GetVideoBitrateParam(state, videoCodec); var framerate = GetFramerateParam(state); if (framerate.HasValue) @@ -644,29 +665,66 @@ namespace MediaBrowser.MediaEncoding.Encoder param += " -profile:v " + state.Options.Profile; } - if (state.Options.Level.HasValue) + var levelString = state.Options.Level.HasValue ? state.Options.Level.Value.ToString(CultureInfo.InvariantCulture) : null; + + if (!string.IsNullOrEmpty(levelString)) { - param += " -level " + state.Options.Level.Value.ToString(UsCulture); + var h264Encoder = EncodingJobFactory.GetH264Encoder(state, GetEncodingOptions()); + + // h264_qsv and libnvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format + if (String.Equals(h264Encoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || String.Equals(h264Encoder, "libnvenc", StringComparison.OrdinalIgnoreCase)) + { + switch (levelString) + { + case "30": + param += " -level 3"; + break; + case "31": + param += " -level 3.1"; + break; + case "32": + param += " -level 3.2"; + break; + case "40": + param += " -level 4"; + break; + case "41": + param += " -level 4.1"; + break; + case "42": + param += " -level 4.2"; + break; + case "50": + param += " -level 5"; + break; + case "51": + param += " -level 5.1"; + break; + case "52": + param += " -level 5.2"; + break; + default: + param += " -level " + levelString; + break; + } + } + else + { + param += " -level " + levelString; + } } return "-pix_fmt yuv420p " + param; } - protected string GetVideoBitrateParam(EncodingJob state, string videoCodec, bool isHls) + protected string GetVideoBitrateParam(EncodingJob state, string videoCodec) { var bitrate = state.OutputVideoBitrate; if (bitrate.HasValue) { - var hasFixedResolution = state.Options.HasFixedResolution; - if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) { - if (hasFixedResolution) - { - return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture)); - } - // With vpx when crf is used, b:v becomes a max rate // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up. return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture)); @@ -677,18 +735,8 @@ namespace MediaBrowser.MediaEncoding.Encoder return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); } - // H264 - if (hasFixedResolution) - { - if (isHls) - { - return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture)); - } - - return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); - } - - return string.Format(" -maxrate {0} -bufsize {1}", + // h264 + return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", bitrate.Value.ToString(UsCulture), (bitrate.Value * 2).ToString(UsCulture)); } @@ -698,20 +746,23 @@ namespace MediaBrowser.MediaEncoding.Encoder protected double? GetFramerateParam(EncodingJob state) { - if (state.Options.Framerate.HasValue) + if (state.Options != null) { - return state.Options.Framerate.Value; - } + if (state.Options.Framerate.HasValue) + { + return state.Options.Framerate.Value; + } - var maxrate = state.Options.MaxFramerate; + var maxrate = state.Options.MaxFramerate; - if (maxrate.HasValue && state.VideoStream != null) - { - var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; - - if (contentRate.HasValue && contentRate.Value > maxrate.Value) + if (maxrate.HasValue && state.VideoStream != null) { - return maxrate; + var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; + + if (contentRate.HasValue && contentRate.Value > maxrate.Value) + { + return maxrate; + } } } @@ -852,7 +903,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var maxWidthParam = request.MaxWidth.Value.ToString(UsCulture); - filters.Add(string.Format("scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam)); + filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); } // If a max height was requested @@ -863,6 +914,14 @@ namespace MediaBrowser.MediaEncoding.Encoder filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(ih\\,{0})", maxHeightParam)); } + if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + if (filters.Count > 1) + { + //filters[filters.Count - 1] += ":flags=fast_bilinear"; + } + } + var output = string.Empty; if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream) @@ -917,8 +976,10 @@ namespace MediaBrowser.MediaEncoding.Encoder seconds.ToString(UsCulture)); } + var mediaPath = state.MediaPath ?? string.Empty; + return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB", - MediaEncoder.EscapeSubtitleFilterPath(state.MediaPath), + MediaEncoder.EscapeSubtitleFilterPath(mediaPath), state.InternalSubtitleStreamOffset.ToString(UsCulture), seconds.ToString(UsCulture)); } diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs index 9cdc4a7bf..252386af0 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingJobFactory.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -66,38 +67,56 @@ namespace MediaBrowser.MediaEncoding.Encoder ? mediaSources.First() : mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId)); - AttachMediaStreamInfo(state, mediaSource, options); + var videoRequest = state.Options; - state.OutputAudioBitrate = GetAudioBitrateParam(request, state.AudioStream); + AttachMediaSourceInfo(state, mediaSource, videoRequest); + + //var container = Path.GetExtension(state.RequestedUrl); + + //if (string.IsNullOrEmpty(container)) + //{ + // container = request.Static ? + // state.InputContainer : + // (Path.GetExtension(GetOutputFilePath(state)) ?? string.Empty).TrimStart('.'); + //} + + //state.OutputContainer = (container ?? string.Empty).TrimStart('.'); + + state.OutputAudioBitrate = GetAudioBitrateParam(state.Options, state.AudioStream); state.OutputAudioSampleRate = request.AudioSampleRate; - state.OutputAudioCodec = GetAudioCodec(request); + state.OutputAudioCodec = state.Options.AudioCodec; - state.OutputAudioChannels = GetNumAudioChannelsParam(request, state.AudioStream, state.OutputAudioCodec); + state.OutputAudioChannels = GetNumAudioChannelsParam(state.Options, state.AudioStream, state.OutputAudioCodec); - if (isVideoRequest) + if (videoRequest != null) { - state.OutputVideoCodec = GetVideoCodec(request); - state.OutputVideoBitrate = GetVideoBitrateParamValue(request, state.VideoStream); + state.OutputVideoCodec = state.Options.VideoCodec; + state.OutputVideoBitrate = GetVideoBitrateParamValue(state.Options, state.VideoStream); if (state.OutputVideoBitrate.HasValue) { var resolution = ResolutionNormalizer.Normalize( - state.VideoStream == null ? (int?)null : state.VideoStream.BitRate, - state.OutputVideoBitrate.Value, - state.VideoStream == null ? null : state.VideoStream.Codec, + state.VideoStream == null ? (int?)null : state.VideoStream.BitRate, + state.OutputVideoBitrate.Value, + state.VideoStream == null ? null : state.VideoStream.Codec, state.OutputVideoCodec, - request.MaxWidth, - request.MaxHeight); + videoRequest.MaxWidth, + videoRequest.MaxHeight); - request.MaxWidth = resolution.MaxWidth; - request.MaxHeight = resolution.MaxHeight; + videoRequest.MaxWidth = resolution.MaxWidth; + videoRequest.MaxHeight = resolution.MaxHeight; } } ApplyDeviceProfileSettings(state); - TryStreamCopy(state, request); + if (videoRequest != null) + { + TryStreamCopy(state, videoRequest); + } + + //state.OutputFilePath = GetOutputFilePath(state); return state; } @@ -119,7 +138,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - internal static void AttachMediaStreamInfo(EncodingJob state, + internal static void AttachMediaSourceInfo(EncodingJob state, MediaSourceInfo mediaSource, EncodingJobOptions videoRequest) { @@ -131,11 +150,6 @@ namespace MediaBrowser.MediaEncoding.Encoder state.RunTimeTicks = mediaSource.RunTimeTicks; state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; - if (mediaSource.ReadAtNativeFramerate) - { - state.ReadInputAtNativeFramerate = true; - } - if (mediaSource.VideoType.HasValue) { state.VideoType = mediaSource.VideoType.Value; @@ -156,6 +170,7 @@ namespace MediaBrowser.MediaEncoding.Encoder state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.InputBitrate = mediaSource.Bitrate; state.InputFileSize = mediaSource.Size; + state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; if (state.ReadInputAtNativeFramerate || mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) @@ -165,6 +180,12 @@ namespace MediaBrowser.MediaEncoding.Encoder state.InputAudioSync = "1"; } + if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase)) + { + // Seeing some stuttering when transcoding wma to audio-only HLS + state.InputAudioSync = "1"; + } + var mediaStreams = mediaSource.MediaStreams; if (videoRequest != null) @@ -210,19 +231,21 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <returns>System.Nullable{VideoCodecs}.</returns> private static string InferVideoCodec(string container) { - if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) + var ext = "." + (container ?? string.Empty); + + if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) { return "wmv"; } - if (string.Equals(container, "webm", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { return "vpx"; } - if (string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } - if (string.Equals(container, "m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; } @@ -232,35 +255,37 @@ namespace MediaBrowser.MediaEncoding.Encoder private string InferAudioCodec(string container) { - if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase)) + var ext = "." + (container ?? string.Empty); + + if (string.Equals(ext, ".mp3", StringComparison.OrdinalIgnoreCase)) { return "mp3"; } - if (string.Equals(container, "aac", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) { return "aac"; } - if (string.Equals(container, "wma", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) { return "wma"; } - if (string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } - if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } - if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } - if (string.Equals(container, "webm", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } - if (string.Equals(container, "webma", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } @@ -330,8 +355,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private int? GetNumAudioChannelsParam(EncodingJobOptions request, MediaStream audioStream, string outputAudioCodec) { var inputChannels = audioStream == null - ? null - : audioStream.Channels; + ? null + : audioStream.Channels; if (inputChannels <= 0) { @@ -348,15 +373,15 @@ namespace MediaBrowser.MediaEncoding.Encoder if (request.MaxAudioChannels.HasValue) { + var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1 + ? 2 + : 6; + if (inputChannels.HasValue) { - return Math.Min(request.MaxAudioChannels.Value, inputChannels.Value); + channelLimit = Math.Min(channelLimit, inputChannels.Value); } - var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1 - ? 2 - : 6; - // If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels return Math.Min(request.MaxAudioChannels.Value, channelLimit); } @@ -398,15 +423,8 @@ namespace MediaBrowser.MediaEncoding.Encoder if (bitrate.HasValue) { - var hasFixedResolution = state.Options.HasFixedResolution; - if (string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) { - if (hasFixedResolution) - { - return string.Format(" -minrate:v ({0}*.90) -maxrate:v ({0}*1.10) -bufsize:v {0} -b:v {0}", bitrate.Value.ToString(UsCulture)); - } - // With vpx when crf is used, b:v becomes a max rate // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. But higher bitrate source files -b:v causes judder so limite the bitrate but dont allow it to "saturate" the bitrate. So dont contrain it down just up. return string.Format(" -maxrate:v {0} -bufsize:v ({0}*2) -b:v {0}", bitrate.Value.ToString(UsCulture)); @@ -417,18 +435,8 @@ namespace MediaBrowser.MediaEncoding.Encoder return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); } - // H264 - if (hasFixedResolution) - { - if (isHls) - { - return string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture)); - } - - return string.Format(" -b:v {0}", bitrate.Value.ToString(UsCulture)); - } - - return string.Format(" -maxrate {0} -bufsize {1}", + // h264 + return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", bitrate.Value.ToString(UsCulture), (bitrate.Value * 2).ToString(UsCulture)); } @@ -466,11 +474,11 @@ namespace MediaBrowser.MediaEncoding.Encoder /// <summary> /// Gets the name of the output audio codec /// </summary> - /// <param name="request">The request.</param> + /// <param name="state">The state.</param> /// <returns>System.String.</returns> - private string GetAudioCodec(EncodingJobOptions request) + internal static string GetAudioEncoder(EncodingJob state) { - var codec = request.AudioCodec; + var codec = state.OutputAudioCodec; if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { @@ -489,40 +497,56 @@ namespace MediaBrowser.MediaEncoding.Encoder return "wmav2"; } - return (codec ?? string.Empty).ToLower(); + return codec.ToLower(); } /// <summary> /// Gets the name of the output video codec /// </summary> - /// <param name="request">The request.</param> + /// <param name="state">The state.</param> + /// <param name="options">The options.</param> /// <returns>System.String.</returns> - private string GetVideoCodec(EncodingJobOptions request) + internal static string GetVideoEncoder(EncodingJob state, EncodingOptions options) { - var codec = request.VideoCodec; + var codec = state.OutputVideoCodec; - if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(codec)) { - return "libx264"; - } - if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) - { - return "libx265"; - } - if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) - { - return "libvpx"; - } - if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase)) - { - return "wmv2"; + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + return GetH264Encoder(state, options); + } + if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) + { + return "libvpx"; + } + if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase)) + { + return "wmv2"; + } + if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase)) + { + return "libtheora"; + } + + return codec.ToLower(); } - if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase)) + + return "copy"; + } + + internal static string GetH264Encoder(EncodingJob state, EncodingOptions options) + { + if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { - return "libtheora"; + // It's currently failing on live tv + if (state.RunTimeTicks.HasValue) + { + return "h264_qsv"; + } } - return (codec ?? string.Empty).ToLower(); + return "libx264"; } internal static bool CanStreamCopyVideo(EncodingJobOptions request, MediaStream videoStream) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 2a3a416ad..30e50fecd 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -557,6 +557,8 @@ namespace MediaBrowser.MediaEncoding.Encoder vf = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2"; // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made the scale width to 600 break; + default: + break; } } diff --git a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs index 127145aec..5756566fe 100644 --- a/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/VideoEncoder.cs @@ -21,7 +21,7 @@ namespace MediaBrowser.MediaEncoding.Encoder protected override string GetCommandLineArguments(EncodingJob state) { // Get the output codec name - var videoCodec = state.OutputVideoCodec; + var videoCodec = EncodingJobFactory.GetVideoEncoder(state, GetEncodingOptions()); var format = string.Empty; var keyFrame = string.Empty; @@ -29,6 +29,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.Equals(Path.GetExtension(state.OutputFilePath), ".mp4", StringComparison.OrdinalIgnoreCase) && state.Options.Context == EncodingContext.Streaming) { + // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js format = " -f mp4 -movflags frag_keyframe+empty_moov"; } @@ -53,42 +54,49 @@ namespace MediaBrowser.MediaEncoding.Encoder /// Gets video arguments to pass to ffmpeg /// </summary> /// <param name="state">The state.</param> - /// <param name="codec">The video codec.</param> + /// <param name="videoCodec">The video codec.</param> /// <returns>System.String.</returns> - private string GetVideoArguments(EncodingJob state, string codec) + private string GetVideoArguments(EncodingJob state, string videoCodec) { - var args = "-codec:v:0 " + codec; + var args = "-codec:v:0 " + videoCodec; if (state.EnableMpegtsM2TsMode) { args += " -mpegts_m2ts_mode 1"; } - // See if we can save come cpu cycles by avoiding encoding - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + var isOutputMkv = string.Equals(state.Options.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase); + + if (state.RunTimeTicks.HasValue) { - return state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) ? - args + " -bsf:v h264_mp4toannexb" : - args; + //args += " -copyts -avoid_negative_ts disabled -start_at_zero"; } - if (state.Options.Context == EncodingContext.Streaming) + if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", - 5.ToString(UsCulture)); + if (state.VideoStream != null && IsH264(state.VideoStream) && + (string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) || isOutputMkv)) + { + args += " -bsf:v h264_mp4toannexb"; + } - args += keyFrameArg; + return args; } + var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", + 5.ToString(UsCulture)); + + args += keyFrameArg; + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; // Add resolution params, if specified if (!hasGraphicalSubs) { - args += GetOutputSizeParam(state, codec); + args += GetOutputSizeParam(state, videoCodec); } - var qualityParam = GetVideoQualityParam(state, codec, false); + var qualityParam = GetVideoQualityParam(state, videoCodec); if (!string.IsNullOrEmpty(qualityParam)) { @@ -98,7 +106,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // This is for internal graphical subs if (hasGraphicalSubs) { - args += GetGraphicalSubtitleParam(state, codec); + args += GetGraphicalSubtitleParam(state, videoCodec); } return args; @@ -118,11 +126,11 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Get the output codec name - var codec = state.OutputAudioCodec; + var codec = EncodingJobFactory.GetAudioEncoder(state); var args = "-codec:a:0 " + codec; - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { return args; } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index ba44ed7dd..882b2e1c2 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -122,10 +122,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) .ConfigureAwait(false); - using (var stream = subtitle.Item1) + var inputFormat = subtitle.Item2; + + if (string.Equals(inputFormat, outputFormat, StringComparison.OrdinalIgnoreCase) && TryGetWriter(outputFormat) == null) { - var inputFormat = subtitle.Item2; + return subtitle.Item1; + } + using (var stream = subtitle.Item1) + { return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false); } } @@ -288,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return null; } - private ISubtitleWriter GetWriter(string format) + private ISubtitleWriter TryGetWriter(string format) { if (string.IsNullOrEmpty(format)) { @@ -312,6 +317,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles return new TtmlWriter(); } + return null; + } + + private ISubtitleWriter GetWriter(string format) + { + var writer = TryGetWriter(format); + + if (writer != null) + { + return writer; + } + throw new ArgumentException("Unsupported format: " + format); } diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index b8c64b643..cea13f86e 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -668,6 +668,9 @@ <Compile Include="..\MediaBrowser.Model\FileOrganization\FileSortingStatus.cs"> <Link>FileOrganization\FileSortingStatus.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchInfo.cs"> + <Link>FileOrganization\SmartMatchInfo.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\FileOrganization\TvFileOrganizationOptions.cs"> <Link>FileOrganization\TvFileOrganizationOptions.cs</Link> </Compile> diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index e74468eff..6c484ffc9 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -633,6 +633,9 @@ <Compile Include="..\MediaBrowser.Model\FileOrganization\FileSortingStatus.cs"> <Link>FileOrganization\FileSortingStatus.cs</Link> </Compile> + <Compile Include="..\MediaBrowser.Model\FileOrganization\SmartMatchInfo.cs"> + <Link>FileOrganization\SmartMatchInfo.cs</Link> + </Compile> <Compile Include="..\MediaBrowser.Model\FileOrganization\TvFileOrganizationOptions.cs"> <Link>FileOrganization\TvFileOrganizationOptions.cs</Link> </Compile> diff --git a/MediaBrowser.Model/ApiClient/IApiClient.cs b/MediaBrowser.Model/ApiClient/IApiClient.cs index 6107c0fbe..904beb736 100644 --- a/MediaBrowser.Model/ApiClient/IApiClient.cs +++ b/MediaBrowser.Model/ApiClient/IApiClient.cs @@ -1413,6 +1413,13 @@ namespace MediaBrowser.Model.ApiClient /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<System.Int32>.</returns> - Task<int> GetSupportedBitrate(CancellationToken cancellationToken); + Task<int> DetectMaxBitrate(CancellationToken cancellationToken); + + /// <summary> + /// Gets the end point information. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>System.Threading.Tasks.Task<MediaBrowser.Model.Net.EndPointInfo>.</returns> + Task<EndPointInfo> GetEndPointInfo(CancellationToken cancellationToken); } }
\ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/CinemaModeConfiguration.cs b/MediaBrowser.Model/Configuration/CinemaModeConfiguration.cs index 764a7222f..c4b96ea2e 100644 --- a/MediaBrowser.Model/Configuration/CinemaModeConfiguration.cs +++ b/MediaBrowser.Model/Configuration/CinemaModeConfiguration.cs @@ -11,6 +11,7 @@ namespace MediaBrowser.Model.Configuration public bool EnableIntrosParentalControl { get; set; } public bool EnableIntrosFromSimilarMovies { get; set; } public string CustomIntroPath { get; set; } + public string MediaInfoIntroPath { get; set; } public bool EnableIntrosFromUpcomingDvdMovies { get; set; } public bool EnableIntrosFromUpcomingStreamingMovies { get; set; } diff --git a/MediaBrowser.Model/Configuration/DlnaOptions.cs b/MediaBrowser.Model/Configuration/DlnaOptions.cs index c1974b877..6eaf01c4a 100644 --- a/MediaBrowser.Model/Configuration/DlnaOptions.cs +++ b/MediaBrowser.Model/Configuration/DlnaOptions.cs @@ -5,7 +5,7 @@ namespace MediaBrowser.Model.Configuration { public bool EnablePlayTo { get; set; } public bool EnableServer { get; set; } - public bool EnableDebugLogging { get; set; } + public bool EnableDebugLog { get; set; } public bool BlastAliveMessages { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; } public int BlastAliveMessageIntervalSeconds { get; set; } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 152bdd993..3cb543e5d 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -93,24 +93,6 @@ namespace MediaBrowser.Model.Configuration public bool EnableLocalizedGuids { get; set; } /// <summary> - /// Gets or sets a value indicating whether [disable startup scan]. - /// </summary> - /// <value><c>true</c> if [disable startup scan]; otherwise, <c>false</c>.</value> - public bool DisableStartupScan { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable user views]. - /// </summary> - /// <value><c>true</c> if [enable user views]; otherwise, <c>false</c>.</value> - public bool EnableUserViews { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [enable library metadata sub folder]. - /// </summary> - /// <value><c>true</c> if [enable library metadata sub folder]; otherwise, <c>false</c>.</value> - public bool EnableLibraryMetadataSubFolder { get; set; } - - /// <summary> /// Gets or sets the preferred metadata language. /// </summary> /// <value>The preferred metadata language.</value> @@ -179,9 +161,7 @@ namespace MediaBrowser.Model.Configuration /// </summary> /// <value>The dashboard source path.</value> public string DashboardSourcePath { get; set; } - - public bool MergeMetadataAndImagesByName { get; set; } - + /// <summary> /// Gets or sets the image saving convention. /// </summary> @@ -220,18 +200,18 @@ namespace MediaBrowser.Model.Configuration public bool EnableWindowsShortcuts { get; set; } - public bool EnableVideoFrameByFrameAnalysis { get; set; } - public bool EnableDateLastRefresh { get; set; } public string[] Migrations { get; set; } + public int MigrationVersion { get; set; } + /// <summary> /// Initializes a new instance of the <see cref="ServerConfiguration" /> class. /// </summary> public ServerConfiguration() { - Migrations = new string[] {}; + Migrations = new string[] { }; ImageSavingConvention = ImageSavingConvention.Compatible; PublicPort = 8096; @@ -576,7 +556,7 @@ namespace MediaBrowser.Model.Configuration Type = ImageType.Thumb } }, - DisabledMetadataFetchers = new []{ "TheMovieDb" } + DisabledMetadataFetchers = new []{ "The Open Movie Database", "TheMovieDb" } }, new MetadataOptions(0, 1280) @@ -597,6 +577,7 @@ namespace MediaBrowser.Model.Configuration Type = ImageType.Primary } }, + DisabledMetadataFetchers = new []{ "The Open Movie Database" }, DisabledImageFetchers = new []{ "TheMovieDb" } } }; diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 935d280cd..ef2de5d78 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -192,6 +192,7 @@ namespace MediaBrowser.Model.Dto /// <value>The channel identifier.</value> public string ChannelId { get; set; } public string ChannelName { get; set; } + public string ServiceName { get; set; } /// <summary> /// Gets or sets the overview. diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 8897edcbd..f09a8b3a7 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -51,7 +51,7 @@ namespace MediaBrowser.Model.Dto public string TranscodingUrl { get; set; } public string TranscodingSubProtocol { get; set; } public string TranscodingContainer { get; set; } - + public MediaSourceInfo() { Formats = new List<string>(); diff --git a/MediaBrowser.Model/Entities/Video3DFormat.cs b/MediaBrowser.Model/Entities/Video3DFormat.cs index 064b02edd..722df4281 100644 --- a/MediaBrowser.Model/Entities/Video3DFormat.cs +++ b/MediaBrowser.Model/Entities/Video3DFormat.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.Model.Entities HalfSideBySide, FullSideBySide, FullTopAndBottom, - HalfTopAndBottom + HalfTopAndBottom, + MVC } } diff --git a/MediaBrowser.Model/FileOrganization/AutoOrganizeOptions.cs b/MediaBrowser.Model/FileOrganization/AutoOrganizeOptions.cs index ae701ea68..830d55bf5 100644 --- a/MediaBrowser.Model/FileOrganization/AutoOrganizeOptions.cs +++ b/MediaBrowser.Model/FileOrganization/AutoOrganizeOptions.cs @@ -9,9 +9,16 @@ namespace MediaBrowser.Model.FileOrganization /// <value>The tv options.</value> public TvFileOrganizationOptions TvOptions { get; set; } + /// <summary> + /// Gets or sets a list of smart match entries. + /// </summary> + /// <value>The smart match entries.</value> + public SmartMatchInfo[] SmartMatchInfos { get; set; } + public AutoOrganizeOptions() { TvOptions = new TvFileOrganizationOptions(); + SmartMatchInfos = new SmartMatchInfo[]{}; } } } diff --git a/MediaBrowser.Model/FileOrganization/SmartMatchInfo.cs b/MediaBrowser.Model/FileOrganization/SmartMatchInfo.cs new file mode 100644 index 000000000..28c99b89b --- /dev/null +++ b/MediaBrowser.Model/FileOrganization/SmartMatchInfo.cs @@ -0,0 +1,16 @@ + +namespace MediaBrowser.Model.FileOrganization +{ + public class SmartMatchInfo + { + public string ItemName { get; set; } + public string DisplayName { get; set; } + public FileOrganizerType OrganizerType { get; set; } + public string[] MatchStrings { get; set; } + + public SmartMatchInfo() + { + MatchStrings = new string[] { }; + } + } +} diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 2b45422ec..838325d68 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Model.LiveTv public bool EnableMovieProviders { get; set; } public string RecordingPath { get; set; } public bool EnableAutoOrganize { get; set; } + public bool EnableRecordingEncoding { get; set; } public List<TunerHostInfo> TunerHosts { get; set; } public List<ListingsProviderInfo> ListingProviders { get; set; } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index db278baa1..a5191192c 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -137,6 +137,7 @@ <Compile Include="Dto\MetadataEditorInfo.cs" /> <Compile Include="Dto\NameIdPair.cs" /> <Compile Include="Dto\NameValuePair.cs" /> + <Compile Include="FileOrganization\SmartMatchInfo.cs" /> <Compile Include="MediaInfo\LiveStreamRequest.cs" /> <Compile Include="MediaInfo\LiveStreamResponse.cs" /> <Compile Include="MediaInfo\PlaybackInfoRequest.cs" /> diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index e8b1414b7..97fec8fdd 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -253,6 +253,11 @@ /// <summary> /// The season user data /// </summary> - SeasonUserData + SeasonUserData, + + /// <summary> + /// The service name + /// </summary> + ServiceName } } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index cba19a5c2..3cf657c44 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -47,11 +47,11 @@ namespace MediaBrowser.Providers.BoxSets if (mergeMetadataSettings) { - var list = sourceItem.LinkedChildren.Where(i => i.Type != LinkedChildType.Manual).ToList(); + // Retain shortcut children + var linkedChildren = sourceItem.LinkedChildren.ToList(); + linkedChildren.AddRange(sourceItem.LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut)); - list.AddRange(targetItem.LinkedChildren.Where(i => i.Type == LinkedChildType.Manual)); - - targetItem.LinkedChildren = list; + targetItem.LinkedChildren = linkedChildren; targetItem.Shares = sourceItem.Shares; } } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 32ea47425..64a0e6d5b 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -375,7 +375,12 @@ namespace MediaBrowser.Providers.Manager } string filename; - var folderName = item is MusicAlbum || item is MusicArtist ? "folder" : "poster"; + var folderName = item is MusicAlbum || + item is MusicArtist || + item is PhotoAlbum || + (saveLocally && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Legacy) ? + "folder" : + "poster"; switch (type) { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 090509ed3..b0d3f6381 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1022,7 +1022,7 @@ namespace MediaBrowser.Providers.Manager .ToList(); var musicArtists = albums - .Select(i => i.Parent) + .Select(i => i.GetParent()) .OfType<MusicArtist>() .ToList(); diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8f6ef1d8f..24397dd5a 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -177,6 +177,7 @@ <Compile Include="TV\MovieDbSeasonProvider.cs" /> <Compile Include="TV\MovieDbSeriesImageProvider.cs" /> <Compile Include="TV\MovieDbSeriesProvider.cs" /> + <Compile Include="TV\OmdbEpisodeProvider.cs" /> <Compile Include="TV\SeriesMetadataService.cs" /> <Compile Include="TV\TvdbEpisodeImageProvider.cs" /> <Compile Include="People\TvdbPersonImageProvider.cs" /> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 8094aa58d..ebbc045ab 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -223,7 +223,7 @@ namespace MediaBrowser.Providers.MediaInfo FetchEmbeddedInfo(video, mediaInfo, options); await FetchPeople(video, mediaInfo, options).ConfigureAwait(false); - video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270); + video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1260); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); diff --git a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs index a6f346d48..eaebd426f 100644 --- a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs @@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Omdb } // Save the http requests since we know it's not currently supported - if (item is Series || item is Season || item is Episode) + if (item is Season || item is Episode) { return false; } diff --git a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs index 30bd4bed7..ace1a8ffb 100644 --- a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs @@ -65,12 +65,18 @@ namespace MediaBrowser.Providers.Omdb private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, string type, bool enableMultipleResults, CancellationToken cancellationToken) { bool isSearch = false; + var episodeSearchInfo = searchInfo as EpisodeInfo; var list = new List<RemoteSearchResult>(); var imdbId = searchInfo.GetProviderId(MetadataProviders.Imdb); - var url = "http://www.omdbapi.com/?plot=short&r=json"; + var url = "http://www.omdbapi.com/?plot=full&r=json"; + if (type == "episode" && episodeSearchInfo != null) + { + episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out imdbId); + } + var name = searchInfo.Name; var year = searchInfo.Year; @@ -107,6 +113,18 @@ namespace MediaBrowser.Providers.Omdb url += "&i=" + imdbId; } + if (type == "episode") + { + if (searchInfo.IndexNumber.HasValue) + { + url += string.Format(CultureInfo.InvariantCulture, "&Episode={0}", searchInfo.IndexNumber); + } + if (searchInfo.ParentIndexNumber.HasValue) + { + url += string.Format(CultureInfo.InvariantCulture, "&Season={0}", searchInfo.ParentIndexNumber); + } + } + using (var stream = await _httpClient.Get(new HttpRequestOptions { Url = url, @@ -136,10 +154,20 @@ namespace MediaBrowser.Providers.Omdb foreach (var result in resultList) { - var item = new RemoteSearchResult(); + var item = new RemoteSearchResult + { + IndexNumber = searchInfo.IndexNumber, + Name = result.Title, + ParentIndexNumber = searchInfo.ParentIndexNumber, + ProviderIds = searchInfo.ProviderIds, + SearchProviderName = Name + }; + + if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue) + { + item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value; + } - item.SearchProviderName = Name; - item.Name = result.Title; item.SetProviderId(MetadataProviders.Imdb, result.imdbID); int parsedYear; @@ -149,6 +177,13 @@ namespace MediaBrowser.Providers.Omdb item.ProductionYear = parsedYear; } + DateTime released; + if (!string.IsNullOrEmpty(result.Released) + && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out released)) + { + item.PremiereDate = released; + } + if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase)) { item.ImageUrl = result.Poster; @@ -279,6 +314,8 @@ namespace MediaBrowser.Providers.Omdb public string Year { get; set; } public string Rated { get; set; } public string Released { get; set; } + public string Season { get; set; } + public string Episode { get; set; } public string Runtime { get; set; } public string Genre { get; set; } public string Director { get; set; } @@ -293,6 +330,7 @@ namespace MediaBrowser.Providers.Omdb public string imdbRating { get; set; } public string imdbVotes { get; set; } public string imdbID { get; set; } + public string seriesID { get; set; } public string Type { get; set; } public string Response { get; set; } } diff --git a/MediaBrowser.Providers/People/MovieDbPersonProvider.cs b/MediaBrowser.Providers/People/MovieDbPersonProvider.cs index 14304c2eb..5525b547f 100644 --- a/MediaBrowser.Providers/People/MovieDbPersonProvider.cs +++ b/MediaBrowser.Providers/People/MovieDbPersonProvider.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Providers.People var requestCount = _requestCount; - if (requestCount >= 5) + if (requestCount >= 10) { //_logger.Debug("Throttling Tmdb people"); diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs index 0faaaa702..d4e4b4844 100644 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs @@ -175,7 +175,11 @@ namespace MediaBrowser.Providers.TV { _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber); - await _libraryManager.DeleteItem(seasonToRemove).ConfigureAwait(false); + await seasonToRemove.Delete(new DeleteOptions + { + DeleteFileLocation = true + + }).ConfigureAwait(false); hasChanges = true; } diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index 9c0e0b873..ff31dbc92 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -315,7 +315,11 @@ namespace MediaBrowser.Providers.TV { _logger.Info("Removing missing/unaired episode {0} {1}x{2}", episodeToRemove.Series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber); - await _libraryManager.DeleteItem(episodeToRemove).ConfigureAwait(false); + await episodeToRemove.Delete(new DeleteOptions + { + DeleteFileLocation = true + + }).ConfigureAwait(false); hasChanges = true; } @@ -380,7 +384,11 @@ namespace MediaBrowser.Providers.TV { _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber); - await _libraryManager.DeleteItem(seasonToRemove).ConfigureAwait(false); + await seasonToRemove.Delete(new DeleteOptions + { + DeleteFileLocation = true + + }).ConfigureAwait(false); hasChanges = true; } diff --git a/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs new file mode 100644 index 000000000..5a920c37f --- /dev/null +++ b/MediaBrowser.Providers/TV/OmdbEpisodeProvider.cs @@ -0,0 +1,89 @@ +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Omdb; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Providers.TV +{ + class OmdbEpisodeProvider : + IRemoteMetadataProvider<Episode, EpisodeInfo>, + IHasOrder + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IHttpClient _httpClient; + private OmdbItemProvider _itemProvider; + + public OmdbEpisodeProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager) + { + _jsonSerializer = jsonSerializer; + _httpClient = httpClient; + _itemProvider = new OmdbItemProvider(jsonSerializer, httpClient, logger, libraryManager); + } + + public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken); + } + + public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult<Episode> + { + Item = new Episode() + }; + + var imdbId = info.GetProviderId(MetadataProviders.Imdb); + if (string.IsNullOrWhiteSpace(imdbId)) + { + imdbId = await GetEpisodeImdbId(info, cancellationToken).ConfigureAwait(false); + } + + if (!string.IsNullOrEmpty(imdbId)) + { + result.Item.SetProviderId(MetadataProviders.Imdb, imdbId); + result.HasMetadata = true; + + await new OmdbProvider(_jsonSerializer, _httpClient).Fetch(result.Item, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + } + + return result; + } + + private async Task<string> GetEpisodeImdbId(EpisodeInfo info, CancellationToken cancellationToken) + { + var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false); + var first = results.FirstOrDefault(); + return first == null ? null : first.GetProviderId(MetadataProviders.Imdb); + } + + public int Order + { + get + { + // After TheTvDb + return 1; + } + } + + public string Name + { + get { return "The Open Movie Database"; } + } + + public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) + { + return _itemProvider.GetImageResponse(url, cancellationToken); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs b/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs index f491a5c44..3a89d6928 100644 --- a/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs +++ b/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs @@ -14,19 +14,17 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Activity { - public class ActivityRepository : IActivityRepository, IDisposable + public class ActivityRepository : BaseSqliteRepository, IActivityRepository { private IDbConnection _connection; - private readonly ILogger _logger; - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); private readonly IServerApplicationPaths _appPaths; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private IDbCommand _saveActivityCommand; - public ActivityRepository(ILogger logger, IServerApplicationPaths appPaths) + public ActivityRepository(ILogManager logManager, IServerApplicationPaths appPaths) + : base(logManager) { - _logger = logger; _appPaths = appPaths; } @@ -34,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.Activity { var dbFile = Path.Combine(_appPaths.DataPath, "activitylog.db"); - _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); string[] queries = { @@ -47,7 +45,7 @@ namespace MediaBrowser.Server.Implementations.Activity "pragma shrink_memory" }; - _connection.RunQueries(queries, _logger); + _connection.RunQueries(queries, Logger); PrepareStatements(); } @@ -82,7 +80,7 @@ namespace MediaBrowser.Server.Implementations.Activity throw new ArgumentNullException("entry"); } - await _writeLock.WaitAsync().ConfigureAwait(false); + await WriteLock.WaitAsync().ConfigureAwait(false); IDbTransaction transaction = null; @@ -119,7 +117,7 @@ namespace MediaBrowser.Server.Implementations.Activity } catch (Exception e) { - _logger.ErrorException("Failed to save record:", e); + Logger.ErrorException("Failed to save record:", e); if (transaction != null) { @@ -135,7 +133,7 @@ namespace MediaBrowser.Server.Implementations.Activity transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -264,45 +262,17 @@ namespace MediaBrowser.Server.Implementations.Activity return info; } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() + protected override void CloseConnection() { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _disposeLock = new object(); - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) + if (_connection != null) { - try + if (_connection.IsOpen()) { - lock (_disposeLock) - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing database", ex); + _connection.Close(); } + + _connection.Dispose(); + _connection = null; } } } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index 284556c72..c7865b6aa 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -96,6 +96,11 @@ namespace MediaBrowser.Server.Implementations.Channels .OrderBy(i => i.Name); } + public IEnumerable<Guid> GetInstalledChannelIds() + { + return GetAllChannels().Select(i => GetInternalChannelId(i.Name)); + } + public Task<QueryResult<Channel>> GetChannelsInternal(ChannelQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrWhiteSpace(query.UserId) @@ -402,25 +407,15 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken) { + var parentFolder = await GetInternalChannelFolder(cancellationToken).ConfigureAwait(false); + var parentFolderId = parentFolder.Id; + var id = GetInternalChannelId(channelInfo.Name); var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id); var isNew = false; - - if (!_fileSystem.DirectoryExists(path)) - { - _logger.Debug("Creating directory {0}", path); - - _fileSystem.CreateDirectory(path); - - if (!_fileSystem.DirectoryExists(path)) - { - throw new IOException("Path not created: " + path); - } - - isNew = true; - } + var forceUpdate = false; var item = _libraryManager.GetItemById(id) as Channel; var channelId = channelInfo.Name.GetMD5().ToString("N"); @@ -432,18 +427,29 @@ namespace MediaBrowser.Server.Implementations.Channels Name = channelInfo.Name, Id = id, DateCreated = _fileSystem.GetCreationTimeUtc(path), - DateModified = _fileSystem.GetLastWriteTimeUtc(path), - Path = path, - ChannelId = channelId + DateModified = _fileSystem.GetLastWriteTimeUtc(path) }; isNew = true; } - if (!string.Equals(item.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { isNew = true; } + item.Path = path; + + if (!string.Equals(item.ChannelId, channelId, StringComparison.OrdinalIgnoreCase)) + { + forceUpdate = true; + } + item.ChannelId = channelId; + + if (item.ParentId != parentFolderId) + { + forceUpdate = true; + } + item.ParentId = parentFolderId; item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); item.Overview = channelInfo.Description; @@ -453,13 +459,17 @@ namespace MediaBrowser.Server.Implementations.Channels { item.Name = channelInfo.Name; } - - await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) - { - ForceSave = isNew - }, cancellationToken); + if (isNew) + { + await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); + } + else if (forceUpdate) + { + await item.UpdateToRepository(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); + } + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem), cancellationToken); return item; } @@ -1219,6 +1229,7 @@ namespace MediaBrowser.Server.Implementations.Channels { BaseItem item; bool isNew; + bool forceUpdate = false; if (info.Type == ChannelItemType.Folder) { @@ -1248,24 +1259,25 @@ namespace MediaBrowser.Server.Implementations.Channels item.ProductionYear = info.ProductionYear; item.ProviderIds = info.ProviderIds; item.OfficialRating = info.OfficialRating; - item.DateCreated = info.DateCreated ?? DateTime.UtcNow; + item.Tags = info.Tags; } var channelItem = (IChannelItem)item; channelItem.ChannelId = internalChannelId.ToString("N"); - if (!string.Equals(channelItem.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase)) + if (item.ParentId != internalChannelId) { - isNew = true; + forceUpdate = true; } - channelItem.ExternalId = info.Id; + item.ParentId = internalChannelId; - if (isNew) + if (!string.Equals(channelItem.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase)) { - channelItem.Tags = info.Tags; + forceUpdate = true; } + channelItem.ExternalId = info.Id; var channelMediaItem = item as IChannelMediaItem; @@ -1294,6 +1306,10 @@ namespace MediaBrowser.Server.Implementations.Channels await _libraryManager.UpdatePeople(item, info.People ?? new List<PersonInfo>()).ConfigureAwait(false); } } + else if (forceUpdate) + { + await item.UpdateToRepository(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); + } return item; } @@ -1518,4 +1534,4 @@ namespace MediaBrowser.Server.Implementations.Channels } } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs index 2e9d42f49..da4a72cd4 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -123,15 +123,15 @@ namespace MediaBrowser.Server.Implementations.Channels private async Task CleanDatabase(CancellationToken cancellationToken) { - var allChannels = await _channelManager.GetChannelsInternal(new ChannelQuery { }, cancellationToken); + var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); - var allIds = _libraryManager.GetItemIds(new InternalItemsQuery + var databaseIds = _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Channel).Name } }); - var invalidIds = allIds - .Except(allChannels.Items.Select(i => i.Id).ToList()) + var invalidIds = databaseIds + .Except(installedChannelIds) .ToList(); foreach (var id in invalidIds) diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs b/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs index 9ea457284..7ed0d43b1 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs @@ -59,7 +59,7 @@ namespace MediaBrowser.Server.Implementations.Collections return subItem; } - var parent = subItem.Parent; + var parent = subItem.GetParent(); if (parent != null && parent.HasImage(ImageType.Primary)) { @@ -78,24 +78,9 @@ namespace MediaBrowser.Server.Implementations.Collections return Task.FromResult(GetFinalItems(items, 2)); } - protected override async Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) { - var image = itemsWithImages - .Where(i => i.HasImage(ImageType.Primary) && i.GetImageInfo(ImageType.Primary, 0).IsLocalFile && Path.HasExtension(i.GetImagePath(ImageType.Primary))) - .Select(i => i.GetImagePath(ImageType.Primary)) - .FirstOrDefault(); - - if (string.IsNullOrWhiteSpace(image)) - { - return null; - } - - var ext = Path.GetExtension(image); - - var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ext); - File.Copy(image, outputPath); - - return outputPath; + return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); } } } diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs index 5c98e401f..4e742ca7a 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs @@ -40,6 +40,7 @@ namespace MediaBrowser.Server.Implementations.Collections public Folder GetCollectionsFolder(string userId) { return _libraryManager.RootFolder.Children.OfType<ManualCollectionsFolder>() + .FirstOrDefault() ?? _libraryManager.GetUserRootFolder().Children.OfType<ManualCollectionsFolder>() .FirstOrDefault(); } diff --git a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs index d62918d56..7a1d86047 100644 --- a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs @@ -3,7 +3,7 @@ using System.Linq; namespace MediaBrowser.Server.Implementations.Collections { - public class ManualCollectionsFolder : BasePluginFolder + public class ManualCollectionsFolder : BasePluginFolder, IHiddenFromDisplay { public ManualCollectionsFolder() { @@ -11,11 +11,6 @@ namespace MediaBrowser.Server.Implementations.Collections DisplayMediaType = "CollectionFolder"; } - public override bool IsVisible(User user) - { - return base.IsVisible(user) && GetChildren(user, false).Any(); - } - public override bool IsHidden { get @@ -24,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.Collections } } - public override bool IsHiddenFromUser(User user) + public bool IsHiddenFromUser(User user) { return !user.Configuration.DisplayCollectionsView; } @@ -36,7 +31,7 @@ namespace MediaBrowser.Server.Implementations.Collections public override string GetClientTypeName() { - return typeof (CollectionFolder).Name; + return typeof(CollectionFolder).Name; } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs index d7df37332..1a5c35df8 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -35,7 +35,6 @@ namespace MediaBrowser.Server.Implementations.Configuration public ServerConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem) : base(applicationPaths, logManager, xmlSerializer, fileSystem) { - UpdateItemsByNamePath(); UpdateMetadataPath(); } @@ -73,7 +72,6 @@ namespace MediaBrowser.Server.Implementations.Configuration /// </summary> protected override void OnConfigurationUpdated() { - UpdateItemsByNamePath(); UpdateMetadataPath(); base.OnConfigurationUpdated(); @@ -87,19 +85,6 @@ namespace MediaBrowser.Server.Implementations.Configuration } /// <summary> - /// Updates the items by name path. - /// </summary> - private void UpdateItemsByNamePath() - { - if (!Configuration.MergeMetadataAndImagesByName) - { - ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = string.IsNullOrEmpty(Configuration.ItemsByNamePath) ? - null : - Configuration.ItemsByNamePath; - } - } - - /// <summary> /// Updates the metadata path. /// </summary> private void UpdateMetadataPath() @@ -163,12 +148,36 @@ namespace MediaBrowser.Server.Implementations.Configuration ValidateItemByNamePath(newConfig); ValidatePathSubstitutions(newConfig); ValidateMetadataPath(newConfig); + ValidateSslCertificate(newConfig); EventHelper.FireEventIfNotNull(ConfigurationUpdating, this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig }, Logger); base.ReplaceConfiguration(newConfiguration); } + + /// <summary> + /// Validates the SSL certificate. + /// </summary> + /// <param name="newConfig">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + private void ValidateSslCertificate(BaseApplicationConfiguration newConfig) + { + var serverConfig = (ServerConfiguration)newConfig; + + var newPath = serverConfig.CertificatePath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(Configuration.CertificatePath ?? string.Empty, newPath)) + { + // Validate + if (!FileSystem.FileExists(newPath)) + { + throw new FileNotFoundException(string.Format("Certificate file '{0}' does not exist.", newPath)); + } + } + } + private void ValidatePathSubstitutions(ServerConfiguration newConfig) { foreach (var map in newConfig.PathSubstitutions) diff --git a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs index 6d7265972..947933561 100644 --- a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs +++ b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs @@ -3,12 +3,15 @@ using MediaBrowser.Controller.Entities; using System; using System.IO; using System.Linq; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; using CommonIO; -using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Providers; namespace MediaBrowser.Server.Implementations.Devices { - public class CameraUploadsFolder : BasePluginFolder + public class CameraUploadsFolder : BasePluginFolder, ISupportsUserSpecificView { public CameraUploadsFolder() { @@ -21,32 +24,41 @@ namespace MediaBrowser.Server.Implementations.Devices { return false; } - - return GetChildren(user, true).Any() && - base.IsVisible(user); + + return base.IsVisible(user) && HasChildren(); } - public override bool IsHidden + public override string CollectionType { - get - { - return base.IsHidden || !Children.Any(); - } + get { return Model.Entities.CollectionType.Photos; } } - public override bool IsHiddenFromUser(User user) + public override string GetClientTypeName() { - return false; + return typeof(CollectionFolder).Name; } - public override string CollectionType + private bool? _hasChildren; + private bool HasChildren() { - get { return Model.Entities.CollectionType.Photos; } + if (!_hasChildren.HasValue) + { + _hasChildren = LibraryManager.GetItemIds(new InternalItemsQuery { ParentId = Id }).Count > 0; + } + + return _hasChildren.Value; } - public override string GetClientTypeName() + protected override Task ValidateChildrenInternal(IProgress<double> progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) { - return typeof(CollectionFolder).Name; + _hasChildren = null; + return base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService); + } + + [IgnoreDataMember] + public bool EnableUserSpecificView + { + get { return true; } } } @@ -65,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.Devices { var path = Path.Combine(_appPaths.DataPath, "camerauploads"); - _fileSystem.CreateDirectory(path); + _fileSystem.CreateDirectory(path); return new CameraUploadsFolder { diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs index 0b2c082a8..6b1af8d2d 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs +++ b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs @@ -1,6 +1,5 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; @@ -18,6 +17,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Configuration; namespace MediaBrowser.Server.Implementations.Devices { @@ -27,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.Devices private readonly IUserManager _userManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _libraryMonitor; - private readonly IConfigurationManager _config; + private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly INetworkManager _network; @@ -38,7 +38,7 @@ namespace MediaBrowser.Server.Implementations.Devices /// </summary> public event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated; - public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IConfigurationManager config, ILogger logger, INetworkManager network) + public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IServerConfigurationManager config, ILogger logger, INetworkManager network) { _repo = repo; _userManager = userManager; @@ -187,11 +187,6 @@ namespace MediaBrowser.Server.Implementations.Devices } } - private string GetUploadPath(string deviceId) - { - return GetUploadPath(GetDevice(deviceId)); - } - private string GetUploadPath(DeviceInfo device) { if (!string.IsNullOrWhiteSpace(device.CameraUploadPath)) @@ -205,7 +200,7 @@ namespace MediaBrowser.Server.Implementations.Devices return config.CameraUploadPath; } - var path = Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); + var path = DefaultCameraUploadsPath; if (config.EnableCameraUploadSubfolders) { @@ -215,6 +210,11 @@ namespace MediaBrowser.Server.Implementations.Devices return path; } + private string DefaultCameraUploadsPath + { + get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); } + } + public async Task UpdateDeviceInfo(string id, DeviceOptions options) { var device = GetDevice(id); diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 0cd551f6a..590c5fd3f 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -163,16 +163,11 @@ namespace MediaBrowser.Server.Implementations.Dto if (person != null) { - var items = _libraryManager.GetItems(new InternalItemsQuery + var items = _libraryManager.GetItems(new InternalItemsQuery(user) { Person = byName.Name - }).Items; - - if (user != null) - { - return items.Where(i => i.IsVisibleStandalone(user)).ToList(); - } + }, new string[] { }); return items.ToList(); } @@ -217,17 +212,17 @@ namespace MediaBrowser.Server.Implementations.Dto }).Items; } - public void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user) + public void FillSyncInfo(IEnumerable<Tuple<BaseItem, BaseItemDto>> tuples, DtoOptions options, User user) { if (options.Fields.Contains(ItemFields.SyncInfo)) { var syncProgress = GetSyncedItemProgress(options); - foreach (var dto in dtos) + foreach (var tuple in tuples) { - var item = _libraryManager.GetItemById(dto.Id); + var item = tuple.Item1; - FillSyncInfo(dto, item, syncProgress, options, user); + FillSyncInfo(tuple.Item2, item, syncProgress, options, user); } } } @@ -361,6 +356,8 @@ namespace MediaBrowser.Server.Implementations.Dto var collectionFolder = item as ICollectionFolder; if (collectionFolder != null) { + dto.OriginalCollectionType = collectionFolder.CollectionType; + dto.CollectionType = user == null ? collectionFolder.CollectionType : collectionFolder.GetViewType(user); @@ -398,7 +395,7 @@ namespace MediaBrowser.Server.Implementations.Dto else if (item is LiveTvProgram) { - _livetvManager().AddInfoToProgramDto(item, dto, fields.Contains(ItemFields.ChannelInfo), user); + _livetvManager().AddInfoToProgramDto(item, dto, fields, user); } return dto; @@ -468,13 +465,15 @@ namespace MediaBrowser.Server.Implementations.Dto var folder = (Folder)item; - dto.ChildCount = GetChildCount(folder, user); - - // These are just far too slow. - // TODO: Disable for CollectionFolder - if (!(folder is UserRootFolder) && !(folder is UserView)) + if (!(folder is IChannelItem) && !(folder is Channel)) { - SetSpecialCounts(folder, user, dto, fields, syncProgress); + dto.ChildCount = GetChildCount(folder, user); + + // These are just far too slow. + if (!(folder is UserRootFolder) && !(folder is UserView) && !(folder is ICollectionFolder)) + { + SetSpecialCounts(folder, user, dto, fields, syncProgress); + } } dto.UserData.Played = dto.UserData.PlayedPercentage.HasValue && dto.UserData.PlayedPercentage.Value >= 100; @@ -815,7 +814,7 @@ namespace MediaBrowser.Server.Implementations.Dto /// <returns>BaseItem.</returns> private BaseItem GetParentBackdropItem(BaseItem item, BaseItem owner) { - var parent = item.Parent ?? owner; + var parent = item.GetParent() ?? owner; while (parent != null) { @@ -824,7 +823,7 @@ namespace MediaBrowser.Server.Implementations.Dto return parent; } - parent = parent.Parent; + parent = parent.GetParent(); } return null; @@ -839,7 +838,7 @@ namespace MediaBrowser.Server.Implementations.Dto /// <returns>BaseItem.</returns> private BaseItem GetParentImageItem(BaseItem item, ImageType type, BaseItem owner) { - var parent = item.Parent ?? owner; + var parent = item.GetParent() ?? owner; while (parent != null) { @@ -848,7 +847,7 @@ namespace MediaBrowser.Server.Implementations.Dto return parent; } - parent = parent.Parent; + parent = parent.GetParent(); } return null; @@ -1042,7 +1041,11 @@ namespace MediaBrowser.Server.Implementations.Dto dto.IsFolder = item.IsFolder; dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; - dto.IsHD = item.IsHD; + if (item.IsHD.HasValue && item.IsHD.Value) + { + dto.IsHD = item.IsHD; + } + dto.Audio = item.Audio; dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; @@ -1209,15 +1212,15 @@ namespace MediaBrowser.Server.Implementations.Dto dto.VoteCount = item.VoteCount; } - if (item.IsFolder) - { - var folder = (Folder)item; + //if (item.IsFolder) + //{ + // var folder = (Folder)item; - if (fields.Contains(ItemFields.IndexOptions)) - { - dto.IndexOptions = folder.IndexByOptionStrings.ToArray(); - } - } + // if (fields.Contains(ItemFields.IndexOptions)) + // { + // dto.IndexOptions = folder.IndexByOptionStrings.ToArray(); + // } + //} var supportsPlaceHolders = item as ISupportsPlaceHolders; if (supportsPlaceHolders != null) @@ -1520,7 +1523,7 @@ namespace MediaBrowser.Server.Implementations.Dto } dto.ChannelId = item.ChannelId; - + var channelItem = item as IChannelItem; if (channelItem != null) { diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs index be2817fd2..47c754643 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IHttpClient _httpClient; private readonly IUserManager _userManager; private readonly ILogger _logger; - private const string MbAdminUrl = "http://www.mb3admin.com/admin/"; + private const string MbAdminUrl = "https://www.mb3admin.com/admin/"; public UsageReporter(IApplicationHost applicationHost, IHttpClient httpClient, IUserManager userManager, ILogger logger) { diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 008363ca4..b059e4144 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints // Go up one level for indicators if (baseItem != null) { - var parent = baseItem.Parent; + var parent = baseItem.GetParent(); if (parent != null) { diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index 73cc5ab01..9d43dabcd 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -46,12 +46,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization public Task<FileOrganizationResult> OrganizeEpisodeFile(string path, CancellationToken cancellationToken) { - var options = _config.GetAutoOrganizeOptions().TvOptions; + var options = _config.GetAutoOrganizeOptions(); return OrganizeEpisodeFile(path, options, false, cancellationToken); } - public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting, CancellationToken cancellationToken) + public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken) { _logger.Info("Sorting file {0}", path); @@ -110,6 +110,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization premiereDate, options, overwriteExisting, + false, result, cancellationToken).ConfigureAwait(false); } @@ -145,7 +146,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return result; } - public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options, CancellationToken cancellationToken) + public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken) { var result = _organizationService.GetResult(request.ResultId); @@ -159,6 +160,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization null, options, true, + request.RememberCorrection, result, cancellationToken).ConfigureAwait(false); @@ -173,12 +175,13 @@ namespace MediaBrowser.Server.Implementations.FileOrganization int? episodeNumber, int? endingEpiosdeNumber, DateTime? premiereDate, - TvFileOrganizationOptions options, + AutoOrganizeOptions options, bool overwriteExisting, + bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { - var series = GetMatchingSeries(seriesName, result); + var series = GetMatchingSeries(seriesName, result, options); if (series == null) { @@ -197,6 +200,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization premiereDate, options, overwriteExisting, + rememberCorrection, result, cancellationToken); } @@ -207,15 +211,18 @@ namespace MediaBrowser.Server.Implementations.FileOrganization int? episodeNumber, int? endingEpiosdeNumber, DateTime? premiereDate, - TvFileOrganizationOptions options, + AutoOrganizeOptions options, bool overwriteExisting, + bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); + var originalExtractedSeriesString = result.ExtractedName; + // Proceed to sort the file - var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options, cancellationToken).ConfigureAwait(false); + var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(newPath)) { @@ -234,7 +241,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (!overwriteExisting) { - if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) + if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath); result.Status = FileSortingStatus.SkippedExisting; @@ -251,7 +258,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } } - PerformFileSorting(options, result); + PerformFileSorting(options.TvOptions, result); if (overwriteExisting) { @@ -285,6 +292,35 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } } } + + if (rememberCorrection) + { + SaveSmartMatchString(originalExtractedSeriesString, series, options); + } + } + + private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options) + { + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase)); + + if (info == null) + { + info = new SmartMatchInfo(); + info.ItemName = series.Name; + info.OrganizerType = FileOrganizerType.Episode; + info.DisplayName = series.Name; + var list = options.SmartMatchInfos.ToList(); + list.Add(info); + options.SmartMatchInfos = list.ToArray(); + } + + if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase)) + { + var list = info.MatchStrings.ToList(); + list.Add(matchString); + info.MatchStrings = list.ToArray(); + _config.SaveAutoOrganizeOptions(options); + } } private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath) @@ -331,7 +367,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization int? endingEpisodeNumber) { // TODO: Support date-naming? - if (!seasonNumber.HasValue || episodeNumber.HasValue) + if (!seasonNumber.HasValue || !episodeNumber.HasValue) { return new List<string>(); } @@ -435,7 +471,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } } - private Series GetMatchingSeries(string seriesName, FileOrganizationResult result) + private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options) { var parsedName = _libraryManager.ParseName(seriesName); @@ -445,13 +481,28 @@ namespace MediaBrowser.Server.Implementations.FileOrganization result.ExtractedName = nameWithoutYear; result.ExtractedYear = yearInName; - return _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series) + var series = _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series) .Cast<Series>() .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i)) .Where(i => i.Item2 > 0) .OrderByDescending(i => i.Item2) .Select(i => i.Item1) .FirstOrDefault(); + + if (series == null) + { + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(e => e.MatchStrings.Contains(seriesName, StringComparer.OrdinalIgnoreCase)); + + if (info != null) + { + series = _libraryManager.RootFolder + .GetRecursiveChildren(i => i is Series) + .Cast<Series>() + .FirstOrDefault(i => string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase)); + } + } + + return series ?? new Series(); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs b/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs index e43ab3665..c560152db 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs @@ -10,6 +10,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { return manager.GetConfiguration<AutoOrganizeOptions>("autoorganize"); } + public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options) + { + manager.SaveConfiguration("autoorganize", options); + } } public class AutoOrganizeOptionsFactory : IConfigurationFactory diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs index 839a85adb..cf1387b0e 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -11,6 +11,7 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; @@ -96,9 +97,9 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return _repo.Delete(resultId); } - private TvFileOrganizationOptions GetTvOptions() + private AutoOrganizeOptions GetAutoOrganizeptions() { - return _config.GetAutoOrganizeOptions().TvOptions; + return _config.GetAutoOrganizeOptions(); } public async Task PerformOrganization(string resultId) @@ -113,7 +114,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - await organizer.OrganizeEpisodeFile(result.OriginalPath, GetTvOptions(), true, CancellationToken.None) + await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeptions(), true, CancellationToken.None) .ConfigureAwait(false); } @@ -127,7 +128,58 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - await organizer.OrganizeWithCorrection(request, GetTvOptions(), CancellationToken.None).ConfigureAwait(false); + await organizer.OrganizeWithCorrection(request, GetAutoOrganizeptions(), CancellationToken.None).ConfigureAwait(false); + } + + public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var options = GetAutoOrganizeptions(); + + var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray(); + + return new QueryResult<SmartMatchInfo>() + { + Items = items, + TotalRecordCount = options.SmartMatchInfos.Length + }; + } + + public void DeleteSmartMatchEntry(string itemName, string matchString) + { + if (string.IsNullOrEmpty(itemName)) + { + throw new ArgumentNullException("itemName"); + } + + if (string.IsNullOrEmpty(matchString)) + { + throw new ArgumentNullException("matchString"); + } + + var options = GetAutoOrganizeptions(); + + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName)); + + if (info != null && info.MatchStrings.Contains(matchString)) + { + var list = info.MatchStrings.ToList(); + list.Remove(matchString); + info.MatchStrings = list.ToArray(); + + if (info.MatchStrings.Length == 0) + { + var infos = options.SmartMatchInfos.ToList(); + infos.Remove(info); + options.SmartMatchInfos = infos.ToArray(); + } + + _config.SaveAutoOrganizeOptions(options); + } } } } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index f1fe5539f..ace3b5af7 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -50,17 +50,17 @@ namespace MediaBrowser.Server.Implementations.FileOrganization get { return "Library"; } } - private TvFileOrganizationOptions GetTvOptions() + private AutoOrganizeOptions GetAutoOrganizeOptions() { - return _config.GetAutoOrganizeOptions().TvOptions; + return _config.GetAutoOrganizeOptions(); } public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - if (GetTvOptions().IsEnabled) + if (GetAutoOrganizeOptions().TvOptions.IsEnabled) { await new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config, _providerManager) - .Organize(GetTvOptions(), cancellationToken, progress).ConfigureAwait(false); + .Organize(GetAutoOrganizeOptions(), cancellationToken, progress).ConfigureAwait(false); } } @@ -74,12 +74,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization public bool IsHidden { - get { return !GetTvOptions().IsEnabled; } + get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled; } } public bool IsEnabled { - get { return GetTvOptions().IsEnabled; } + get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; } } public bool IsActivityLogged diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs index 3e5296639..845fcdff2 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -52,13 +52,13 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return false; } - public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> progress) + public async Task Organize(AutoOrganizeOptions options, CancellationToken cancellationToken, IProgress<double> progress) { - var watchLocations = options.WatchLocations.ToList(); + var watchLocations = options.TvOptions.WatchLocations.ToList(); var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) .OrderBy(_fileSystem.GetCreationTimeUtc) - .Where(i => EnableOrganization(i, options)) + .Where(i => EnableOrganization(i, options.TvOptions)) .ToList(); var processedFolders = new HashSet<string>(); @@ -76,7 +76,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization try { - var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); + var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.TvOptions.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) { processedFolders.Add(file.DirectoryName); @@ -100,7 +100,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization foreach (var path in processedFolders) { - var deleteExtensions = options.LeftOverFileExtensionsToDelete + var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) @@ -111,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization DeleteLeftOverFiles(path, deleteExtensions); } - if (options.DeleteEmptyFolders) + if (options.TvOptions.DeleteEmptyFolders) { if (!IsWatchFolder(path, watchLocations)) { diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 038116703..a11eb3aaa 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -241,7 +241,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } catch (Exception errorEx) { - _logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx); + //_logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx); } } @@ -266,13 +266,26 @@ namespace MediaBrowser.Server.Implementations.HttpServer {".html", 0} }; - private bool EnableLogging(string url) + private bool EnableLogging(string url, string localPath) { - var parts = url.Split(new[] { '?' }, 2); + var extension = GetExtension(url); + + if (string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension)) + { + if (string.IsNullOrWhiteSpace(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) + { + return true; + } + } + + return false; + } - var extension = Path.GetExtension(parts[0]); + private string GetExtension(string url) + { + var parts = url.Split(new[] { '?' }, 2); - return string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension); + return Path.GetExtension(parts[0]); } /// <summary> @@ -291,7 +304,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer var localPath = url.LocalPath; var urlString = url.OriginalString; - var enableLog = EnableLogging(urlString); + var enableLog = EnableLogging(urlString, localPath); if (enableLog) { @@ -337,8 +350,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (!string.IsNullOrWhiteSpace(GlobalResponse)) { - httpRes.Write(GlobalResponse); - httpRes.ContentType = "text/plain"; + if (string.Equals(GetExtension(urlString), "html", StringComparison.OrdinalIgnoreCase)) + { + httpRes.Write(GlobalResponse); + httpRes.ContentType = "text/plain"; + } + else + { + httpRes.StatusCode = 503; + } + return Task.FromResult(true); } diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 184b72d8f..764eb7c68 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Server.Implementations.IO /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) { - if (e.Item.Parent is AggregateFolder) + if (e.Item.GetParent() is AggregateFolder) { StopWatchingPath(e.Item.Path); } @@ -230,7 +230,7 @@ namespace MediaBrowser.Server.Implementations.IO /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) { - if (e.Item.Parent is AggregateFolder) + if (e.Item.GetParent() is AggregateFolder) { StartWatchingPath(e.Item.Path); } @@ -542,9 +542,16 @@ namespace MediaBrowser.Server.Implementations.IO return false; } + // In order to determine if the file is being written to, we have to request write access + // But if the server only has readonly access, this is going to cause this entire algorithm to fail + // So we'll take a best guess about our access level + var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta + ? FileAccess.ReadWrite + : FileAccess.Read; + try { - using (_fileSystem.GetFileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)) + using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite)) { if (_updateTimer != null) { @@ -661,7 +668,7 @@ namespace MediaBrowser.Server.Implementations.IO // If the item has been deleted find the first valid parent that still exists while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) { - item = item.Parent; + item = item.GetParent(); if (item == null) { diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs index 37131ad2c..5b72860b6 100644 --- a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs +++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -17,6 +17,7 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Common.IO; +using MoreLinq; namespace MediaBrowser.Server.Implementations.Intros { @@ -28,8 +29,9 @@ namespace MediaBrowser.Server.Implementations.Intros private readonly IConfigurationManager _serverConfig; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; + private readonly IMediaSourceManager _mediaSourceManager; - public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig, ILibraryManager libraryManager, IFileSystem fileSystem) + public DefaultIntroProvider(ISecurityManager security, IChannelManager channelManager, ILocalizationManager localization, IConfigurationManager serverConfig, ILibraryManager libraryManager, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) { _security = security; _channelManager = channelManager; @@ -37,6 +39,7 @@ namespace MediaBrowser.Server.Implementations.Intros _serverConfig = serverConfig; _libraryManager = libraryManager; _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; } public async Task<IEnumerable<IntroInfo>> GetIntros(BaseItem item, User user) @@ -78,13 +81,11 @@ namespace MediaBrowser.Server.Implementations.Intros if (config.EnableIntrosFromMoviesInLibrary) { - var inputItems = _libraryManager.GetItems(new InternalItemsQuery + var inputItems = _libraryManager.GetItems(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Movie).Name }, + IncludeItemTypes = new[] { typeof(Movie).Name } - User = user - - }).Items; + }, new string[] { }); var itemsWithTrailers = inputItems .Where(i => @@ -163,7 +164,11 @@ namespace MediaBrowser.Server.Implementations.Intros private IEnumerable<IntroInfo> GetResult(BaseItem item, IEnumerable<ItemWithTrailer> candidates, CinemaModeConfiguration config, int? ratingLevel) { var customIntros = !string.IsNullOrWhiteSpace(config.CustomIntroPath) ? - GetCustomIntros(item) : + GetCustomIntros(config) : + new List<IntroInfo>(); + + var mediaInfoIntros = !string.IsNullOrWhiteSpace(config.MediaInfoIntroPath) ? + GetMediaInfoIntros(config, item) : new List<IntroInfo>(); var trailerLimit = config.TrailerLimit; @@ -187,7 +192,8 @@ namespace MediaBrowser.Server.Implementations.Intros .ThenByDescending(i => (i.IsPlayed ? 0 : 1)) .Select(i => i.IntroInfo) .Take(trailerLimit) - .Concat(customIntros.Take(1)); + .Concat(customIntros.Take(1)) + .Concat(mediaInfoIntros); } private bool IsDuplicate(BaseItem playingContent, BaseItem test) @@ -212,11 +218,11 @@ namespace MediaBrowser.Server.Implementations.Intros return _serverConfig.GetConfiguration<CinemaModeConfiguration>("cinemamode"); } - private List<IntroInfo> GetCustomIntros(BaseItem item) + private List<IntroInfo> GetCustomIntros(CinemaModeConfiguration options) { try { - return GetCustomIntroFiles() + return GetCustomIntroFiles(options, true, false) .OrderBy(i => Guid.NewGuid()) .Select(i => new IntroInfo { @@ -230,17 +236,151 @@ namespace MediaBrowser.Server.Implementations.Intros } } - private IEnumerable<string> GetCustomIntroFiles(CinemaModeConfiguration options = null) + private IEnumerable<IntroInfo> GetMediaInfoIntros(CinemaModeConfiguration options, BaseItem item) + { + try + { + var hasMediaSources = item as IHasMediaSources; + + if (hasMediaSources == null) + { + return new List<IntroInfo>(); + } + + var mediaSource = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false) + .FirstOrDefault(); + + if (mediaSource == null) + { + return new List<IntroInfo>(); + } + + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + var allIntros = GetCustomIntroFiles(options, false, true) + .OrderBy(i => Guid.NewGuid()) + .Select(i => new IntroInfo + { + Path = i + + }).ToList(); + + var returnResult = new List<IntroInfo>(); + + if (videoStream != null) + { + returnResult.AddRange(GetMediaInfoIntrosByVideoStream(allIntros, videoStream).Take(1)); + } + + if (audioStream != null) + { + returnResult.AddRange(GetMediaInfoIntrosByAudioStream(allIntros, audioStream).Take(1)); + } + + returnResult.AddRange(GetMediaInfoIntrosByTags(allIntros, item.Tags).Take(1)); + + return returnResult.DistinctBy(i => i.Path, StringComparer.OrdinalIgnoreCase); + } + catch (IOException) + { + return new List<IntroInfo>(); + } + } + + private IEnumerable<IntroInfo> GetMediaInfoIntrosByVideoStream(List<IntroInfo> allIntros, MediaStream stream) + { + var codec = stream.Codec; + + if (string.IsNullOrWhiteSpace(codec)) + { + return new List<IntroInfo>(); + } + + return allIntros + .Where(i => IsMatch(i.Path, codec)); + } + + private IEnumerable<IntroInfo> GetMediaInfoIntrosByAudioStream(List<IntroInfo> allIntros, MediaStream stream) + { + var codec = stream.Codec; + + if (string.IsNullOrWhiteSpace(codec)) + { + return new List<IntroInfo>(); + } + + return allIntros + .Where(i => IsAudioMatch(i.Path, stream)); + } + + private IEnumerable<IntroInfo> GetMediaInfoIntrosByTags(List<IntroInfo> allIntros, List<string> tags) { - options = options ?? GetOptions(); + return allIntros + .Where(i => tags.Any(t => IsMatch(i.Path, t))); + } + + private bool IsMatch(string file, string attribute) + { + var filename = Path.GetFileNameWithoutExtension(file) ?? string.Empty; + filename = Normalize(filename); + + if (string.IsNullOrWhiteSpace(filename)) + { + return false; + } + + attribute = Normalize(attribute); + if (string.IsNullOrWhiteSpace(attribute)) + { + return false; + } + + return string.Equals(filename, attribute, StringComparison.OrdinalIgnoreCase); + } + + private string Normalize(string value) + { + return value; + } + + private bool IsAudioMatch(string path, MediaStream stream) + { + if (!string.IsNullOrWhiteSpace(stream.Codec)) + { + if (IsMatch(path, stream.Codec)) + { + return true; + } + } + if (!string.IsNullOrWhiteSpace(stream.Profile)) + { + if (IsMatch(path, stream.Profile)) + { + return true; + } + } + + return false; + } + + private IEnumerable<string> GetCustomIntroFiles(CinemaModeConfiguration options, bool enableCustomIntros, bool enableMediaInfoIntros) + { + var list = new List<string>(); + + if (enableCustomIntros && !string.IsNullOrWhiteSpace(options.CustomIntroPath)) + { + list.AddRange(_fileSystem.GetFilePaths(options.CustomIntroPath, true) + .Where(_libraryManager.IsVideoFile)); + } - if (string.IsNullOrWhiteSpace(options.CustomIntroPath)) + if (enableMediaInfoIntros && !string.IsNullOrWhiteSpace(options.MediaInfoIntroPath)) { - return new List<string>(); + list.AddRange(_fileSystem.GetFilePaths(options.MediaInfoIntroPath, true) + .Where(_libraryManager.IsVideoFile)); } - return _fileSystem.GetFilePaths(options.CustomIntroPath, true) - .Where(_libraryManager.IsVideoFile); + return list.Distinct(StringComparer.OrdinalIgnoreCase); } private bool FilterByParentalRating(int? ratingLevel, BaseItem item) @@ -341,7 +481,7 @@ namespace MediaBrowser.Server.Implementations.Intros public IEnumerable<string> GetAllIntroFiles() { - return GetCustomIntroFiles(); + return GetCustomIntroFiles(GetOptions(), true, true); } private bool IsSupporter diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index 9035d6479..402fa439d 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -47,11 +47,14 @@ namespace MediaBrowser.Server.Implementations.Library /// <summary> /// Shoulds the ignore. /// </summary> - /// <param name="args">The args.</param> + /// <param name="fileInfo">The file information.</param> + /// <param name="parent">The parent.</param> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public bool ShouldIgnore(ItemResolveArgs args) + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) { - var filename = args.FileInfo.Name; + var filename = fileInfo.Name; + var isHidden = (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + var path = fileInfo.FullName; // Handle mac .DS_Store // https://github.com/MediaBrowser/MediaBrowser/issues/427 @@ -61,21 +64,24 @@ namespace MediaBrowser.Server.Implementations.Library } // Ignore hidden files and folders - if (args.IsHidden) + if (isHidden) { - var parentFolderName = Path.GetFileName(Path.GetDirectoryName(args.Path)); - - if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + if (parent == null) { - return false; + var parentFolderName = Path.GetFileName(Path.GetDirectoryName(path)); + + if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } } // Sometimes these are marked hidden - if (_fileSystem.IsRootPath(args.Path)) + if (_fileSystem.IsRootPath(path)) { return false; } @@ -83,7 +89,7 @@ namespace MediaBrowser.Server.Implementations.Library return true; } - if (args.IsDirectory) + if (fileInfo.IsDirectory) { // Ignore any folders in our list if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) @@ -91,26 +97,29 @@ namespace MediaBrowser.Server.Implementations.Library return true; } - // Ignore trailer folders but allow it at the collection level - if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) && - !(args.Parent is AggregateFolder) && !(args.Parent is UserRootFolder)) + if (parent != null) { - return true; - } + // Ignore trailer folders but allow it at the collection level + if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) && + !(parent is AggregateFolder) && !(parent is UserRootFolder)) + { + return true; + } - if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } + if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } - if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) - { - return true; + if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } } } else { - if (args.Parent != null) + if (parent != null) { // Don't resolve these into audio files if (string.Equals(_fileSystem.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) && _libraryManager.IsAudioFile(filename)) diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index b0b2680ca..333b1fbe9 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -27,6 +27,7 @@ using MediaBrowser.Server.Implementations.Library.Validators; using MediaBrowser.Server.Implementations.Logging; using MediaBrowser.Server.Implementations.ScheduledTasks; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -37,6 +38,7 @@ using System.Threading; using System.Threading.Tasks; using CommonIO; using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; using MoreLinq; using SortOrder = MediaBrowser.Model.Entities.SortOrder; @@ -142,6 +144,7 @@ namespace MediaBrowser.Server.Implementations.Library private readonly Func<ILibraryMonitor> _libraryMonitorFactory; private readonly Func<IProviderManager> _providerManagerFactory; + private readonly Func<IUserViewManager> _userviewManager; /// <summary> /// The _library items cache @@ -169,7 +172,7 @@ namespace MediaBrowser.Server.Implementations.Library /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> - public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory) + public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func<ILibraryMonitor> libraryMonitorFactory, IFileSystem fileSystem, Func<IProviderManager> providerManagerFactory, Func<IUserViewManager> userviewManager) { _logger = logger; _taskManager = taskManager; @@ -179,6 +182,7 @@ namespace MediaBrowser.Server.Implementations.Library _libraryMonitorFactory = libraryMonitorFactory; _fileSystem = fileSystem; _providerManagerFactory = providerManagerFactory; + _userviewManager = userviewManager; ByReferenceItems = new ConcurrentDictionary<Guid, BaseItem>(); _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); @@ -403,12 +407,12 @@ namespace MediaBrowser.Server.Implementations.Library { foreach (var path in item.GetDeletePaths().ToList()) { - if (_fileSystem.DirectoryExists(path)) + if (_fileSystem.DirectoryExists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteDirectory(path, true); } - else if (_fileSystem.FileExists(path)) + else if (_fileSystem.FileExists(path)) { _logger.Debug("Deleting path {0}", path); _fileSystem.DeleteFile(path); @@ -582,7 +586,7 @@ namespace MediaBrowser.Server.Implementations.Library }; // Return null if ignore rules deem that we should do so - if (EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(args))) + if (IgnoreFile(args.FileInfo, args.Parent)) { return null; } @@ -618,6 +622,11 @@ namespace MediaBrowser.Server.Implementations.Library return ResolveItem(args); } + public bool IgnoreFile(FileSystemMetadata file, BaseItem parent) + { + return EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent)); + } + public IEnumerable<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths) { var originalList = paths.ToList(); @@ -653,7 +662,7 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, string collectionType) { - var fileList = files.ToList(); + var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList(); if (parent != null) { @@ -704,7 +713,7 @@ namespace MediaBrowser.Server.Implementations.Library { var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath; - _fileSystem.CreateDirectory(rootFolderPath); + _fileSystem.CreateDirectory(rootFolderPath); var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (AggregateFolder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)); @@ -734,6 +743,13 @@ namespace MediaBrowser.Server.Implementations.Library folder = dbItem; } + if (folder.ParentId != rootFolder.Id) + { + folder.ParentId = rootFolder.Id; + var task = folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + Task.WaitAll(task); + } + rootFolder.AddVirtualChild(folder); RegisterItem(folder); @@ -755,7 +771,7 @@ namespace MediaBrowser.Server.Implementations.Library { var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; - _fileSystem.CreateDirectory(userRootPath); + _fileSystem.CreateDirectory(userRootPath); var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder; @@ -1007,9 +1023,9 @@ namespace MediaBrowser.Server.Implementations.Library private void SetPropertiesFromSongs(MusicArtist artist, IEnumerable<IHasMetadata> items) { - + } - + /// <summary> /// Validate and refresh the People sub-set of the IBN. /// The items are stored in the db but not loaded into memory until actually requested by an operation. @@ -1020,7 +1036,7 @@ namespace MediaBrowser.Server.Implementations.Library public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) { // Ensure the location is available. - _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); + _fileSystem.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath); return new PeopleValidator(this, _logger, ConfigurationManager, _fileSystem).ValidatePeople(cancellationToken, progress); } @@ -1267,6 +1283,11 @@ namespace MediaBrowser.Server.Implementations.Library public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + var result = ItemRepository.GetItemIdsList(query); var items = result.Select(GetItemById).Where(i => i != null).ToArray(); @@ -1279,14 +1300,140 @@ namespace MediaBrowser.Server.Implementations.Library public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + return ItemRepository.GetItems(query); } public List<Guid> GetItemIds(InternalItemsQuery query) { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + return ItemRepository.GetItemIdsList(query); } + public IEnumerable<BaseItem> GetItems(InternalItemsQuery query, IEnumerable<string> parentIds) + { + var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + + SetTopParentIdsOrAncestors(query, parents); + + return GetItemIds(query).Select(GetItemById).Where(i => i != null); + } + + public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query, IEnumerable<string> parentIds) + { + var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + + SetTopParentIdsOrAncestors(query, parents); + + return GetItems(query); + } + + private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents) + { + if (parents.All(i => + { + if ((i is ICollectionFolder) || (i is UserView)) + { + return true; + } + + _logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + return false; + + })) + { + // Optimize by querying against top level views + query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + } + else + { + // We need to be able to query from any arbitrary ancestor up the tree + query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray(); + } + } + + private void AddUserToQuery(InternalItemsQuery query, User user) + { + if (query.AncestorIds.Length == 0 && !query.ParentId.HasValue && query.ChannelIds.Length == 0 && query.TopParentIds.Length == 0) + { + var userViews = _userviewManager().GetUserViews(new UserViewQuery + { + UserId = user.Id.ToString("N"), + IncludeHidden = true + + }, CancellationToken.None).Result.ToList(); + + query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray(); + } + } + + private IEnumerable<BaseItem> GetTopParentsForQuery(BaseItem item, User user) + { + var view = item as UserView; + + if (view != null) + { + if (string.Equals(view.ViewType, CollectionType.LiveTv)) + { + return new[] { view }; + } + if (string.Equals(view.ViewType, CollectionType.Channels)) + { + // TODO: Return channels + return new[] { view }; + } + + // Translate view into folders + if (view.DisplayParentId != Guid.Empty) + { + var displayParent = GetItemById(view.DisplayParentId); + if (displayParent != null) + { + return GetTopParentsForQuery(displayParent, user); + } + return new BaseItem[] { }; + } + if (view.ParentId != Guid.Empty) + { + var displayParent = GetItemById(view.ParentId); + if (displayParent != null) + { + return GetTopParentsForQuery(displayParent, user); + } + return new BaseItem[] { }; + } + + // Handle grouping + if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)) + { + var collectionFolders = user.RootFolder.GetChildren(user, true).OfType<CollectionFolder>().Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)); + return collectionFolders.SelectMany(i => GetTopParentsForQuery(i, user)); + } + return new BaseItem[] { }; + } + + var collectionFolder = item as CollectionFolder; + if (collectionFolder != null) + { + return collectionFolder.GetPhysicalParents(); + } + + var topParent = item.GetTopParent(); + if (topParent != null) + { + return new[] { topParent }; + } + return new BaseItem[] { }; + } + /// <summary> /// Gets the intros. /// </summary> @@ -1389,6 +1536,10 @@ namespace MediaBrowser.Server.Implementations.Library { video = dbItem; } + else + { + return null; + } } } catch (Exception ex) @@ -1579,9 +1730,9 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Folder> GetCollectionFolders(BaseItem item) { - while (!(item.Parent is AggregateFolder) && item.Parent != null) + while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null) { - item = item.Parent; + item = item.GetParent(); } if (item == null) @@ -1618,7 +1769,7 @@ namespace MediaBrowser.Server.Implementations.Library return type; } - return item.Parents + return item.GetParents() .Select(GetConfiguredContentType) .LastOrDefault(i => !string.IsNullOrWhiteSpace(i)); } @@ -1655,14 +1806,14 @@ namespace MediaBrowser.Server.Implementations.Library private string GetTopFolderContentType(BaseItem item) { - while (!(item.Parent is AggregateFolder) && item.Parent != null) + if (item == null) { - item = item.Parent; + return null; } - if (item == null) + while (!(item.GetParent() is AggregateFolder) && item.GetParent() != null) { - return null; + item = item.GetParent(); } return GetUserRootFolder().Children @@ -1681,7 +1832,7 @@ namespace MediaBrowser.Server.Implementations.Library string sortName, CancellationToken cancellationToken) { - return GetNamedViewInternal(user, name, null, viewType, sortName, null, cancellationToken); + return GetNamedView(user, name, null, viewType, sortName, cancellationToken); } public async Task<UserView> GetNamedView(string name, @@ -1699,10 +1850,9 @@ namespace MediaBrowser.Server.Implementations.Library var refresh = false; - if (item == null || - !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) + if (item == null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) { - _fileSystem.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -1719,12 +1869,6 @@ namespace MediaBrowser.Server.Implementations.Library refresh = true; } - if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) - { - item.ViewType = viewType; - await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - if (!refresh) { refresh = (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; @@ -1750,40 +1894,14 @@ namespace MediaBrowser.Server.Implementations.Library return item; } - public Task<UserView> GetNamedView(User user, - string name, - string parentId, - string viewType, - string sortName, - string uniqueId, - CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(parentId)) - { - throw new ArgumentNullException("parentId"); - } - - return GetNamedViewInternal(user, name, parentId, viewType, sortName, uniqueId, cancellationToken); - } - - private async Task<UserView> GetNamedViewInternal(User user, + public async Task<UserView> GetNamedView(User user, string name, string parentId, string viewType, string sortName, - string uniqueId, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException("name"); - } - - var idValues = "37_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty); - if (!string.IsNullOrWhiteSpace(uniqueId)) - { - idValues += uniqueId; - } + var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentId ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -1795,7 +1913,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item == null) { - _fileSystem.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -1818,18 +1936,6 @@ namespace MediaBrowser.Server.Implementations.Library isNew = true; } - if (!item.UserId.HasValue) - { - item.UserId = user.Id; - await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - - if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) - { - item.ViewType = viewType; - await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - var refresh = isNew || (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; if (!refresh && item.DisplayParentId != Guid.Empty) @@ -1853,7 +1959,6 @@ namespace MediaBrowser.Server.Implementations.Library public async Task<UserView> GetShadowView(BaseItem parent, string viewType, string sortName, - string uniqueId, CancellationToken cancellationToken) { if (parent == null) @@ -1864,11 +1969,7 @@ namespace MediaBrowser.Server.Implementations.Library var name = parent.Name; var parentId = parent.Id; - var idValues = "37_namedview_" + name + parentId + (viewType ?? string.Empty); - if (!string.IsNullOrWhiteSpace(uniqueId)) - { - idValues += uniqueId; - } + var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -1899,12 +2000,6 @@ namespace MediaBrowser.Server.Implementations.Library isNew = true; } - if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) - { - item.ViewType = viewType; - await item.UpdateToRepository(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - var refresh = isNew || (DateTime.UtcNow - item.DateLastRefreshed) >= _viewRefreshInterval; if (!refresh && item.DisplayParentId != Guid.Empty) @@ -1924,7 +2019,7 @@ namespace MediaBrowser.Server.Implementations.Library return item; } - + public async Task<UserView> GetNamedView(string name, string parentId, string viewType, @@ -1953,7 +2048,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item == null) { - _fileSystem.CreateDirectory(path); + _fileSystem.CreateDirectory(path); item = new UserView { @@ -2200,21 +2295,21 @@ namespace MediaBrowser.Server.Implementations.Library return ResolvePaths(files, directoryService, null, null) .OfType<Video>() .Select(video => - { - // Try to retrieve it from the db. If we don't find it, use the resolved version - var dbItem = GetItemById(video.Id) as Video; - - if (dbItem != null) { - video = dbItem; - } + // Try to retrieve it from the db. If we don't find it, use the resolved version + var dbItem = GetItemById(video.Id) as Video; - video.ExtraType = ExtraType.Trailer; + if (dbItem != null) + { + video = dbItem; + } - return video; + video.ExtraType = ExtraType.Trailer; - // Sort them so that the list can be easily compared for changes - }).OrderBy(i => i.Path).ToList(); + return video; + + // Sort them so that the list can be easily compared for changes + }).OrderBy(i => i.Path).ToList(); } public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) @@ -2386,7 +2481,7 @@ namespace MediaBrowser.Server.Implementations.Library return ItemRepository.UpdatePeople(item.Id, people); } - private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1,1); + private readonly SemaphoreSlim _dynamicImageResourcePool = new SemaphoreSlim(1, 1); public async Task<ItemImageInfo> ConvertImageToLocal(IHasImages item, ItemImageInfo image, int imageIndex) { foreach (var url in image.Path.Split('|')) @@ -2425,4 +2520,4 @@ namespace MediaBrowser.Server.Implementations.Library throw new InvalidOperationException(); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/MusicManager.cs b/MediaBrowser.Server.Implementations/Library/MusicManager.cs index aee101ef4..11a1f190a 100644 --- a/MediaBrowser.Server.Implementations/Library/MusicManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MusicManager.cs @@ -80,15 +80,13 @@ namespace MediaBrowser.Server.Implementations.Library { var genreList = genres.ToList(); - var inputItems = _libraryManager.GetItems(new InternalItemsQuery + var inputItems = _libraryManager.GetItems(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(Audio).Name }, - Genres = genreList.ToArray(), + Genres = genreList.ToArray() - User = user - - }).Items; + }, new string[] { }); var genresDictionary = genreList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs index 4efa1071d..83fdd3da2 100644 --- a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs +++ b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.Library item.Id = libraryManager.GetNewItemId(item.Path, item.GetType()); item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || - item.Parents.Any(i => i.IsLocked); + item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); @@ -78,7 +78,7 @@ namespace MediaBrowser.Server.Implementations.Library EnsureName(item, args.FileInfo); item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 || - item.Parents.Any(i => i.IsLocked); + item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values EnsureDates(fileSystem, item, args, true); diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index fd74b68b8..9edd3f83f 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -191,6 +191,23 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers video.IsHD = true; } } + + SetIsoType(video); + } + + protected void SetIsoType(Video video) + { + if (video.VideoType == VideoType.Iso) + { + if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1) + { + video.IsoType = IsoType.Dvd; + } + else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1) + { + video.IsoType = IsoType.BluRay; + } + } } protected void Set3DFormat(Video video, bool is3D, string format3D) @@ -225,6 +242,10 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers { video.Video3DFormat = Video3DFormat.HalfTopAndBottom; } + else if (string.Equals(format3D, "mvc", StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 3252db505..23424bf3c 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -67,7 +67,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies string collectionType, IDirectoryService directoryService) { - if (IsInvalid(parent, collectionType, files)) + if (IsInvalid(parent, collectionType)) { return null; } @@ -77,16 +77,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<MusicVideo>(parent, files, directoryService, false); } - if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || + string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) { return ResolveVideos<Video>(parent, files, directoryService, false); } - if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) - { - //return ResolveVideos<Video>(parent, files, directoryService, collectionType, false); - } - if (string.IsNullOrEmpty(collectionType)) { // Owned items should just use the plain video type @@ -95,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Video>(parent, files, directoryService, false); } - if (parent is Series || parent.Parents.OfType<Series>().Any()) + if (parent is Series || parent.GetParents().OfType<Series>().Any()) { return null; } @@ -185,7 +181,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies { var collectionType = args.GetCollectionType(); - if (IsInvalid(args.Parent, collectionType, args.FileSystemChildren)) + if (IsInvalid(args.Parent, collectionType)) { return null; } @@ -193,14 +189,18 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies // Find movies with their own folders if (args.IsDirectory) { + var files = args.FileSystemChildren + .Where(i => !LibraryManager.IgnoreFile(i, args.Parent)) + .ToList(); + if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<MusicVideo>(args.Path, args.Parent, args.FileSystemChildren.ToList(), args.DirectoryService, collectionType); + return FindMovie<MusicVideo>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren.ToList(), args.DirectoryService, collectionType); + return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } if (string.IsNullOrEmpty(collectionType)) @@ -208,7 +208,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies // Owned items should just use the plain video type if (args.Parent == null) { - return FindMovie<Video>(args.Path, args.Parent, args.FileSystemChildren.ToList(), args.DirectoryService, collectionType); + return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } if (args.HasParent<Series>()) @@ -216,12 +216,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return null; } - return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren.ToList(), args.DirectoryService, collectionType); + return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)) { - return FindMovie<Movie>(args.Path, args.Parent, args.FileSystemChildren.ToList(), args.DirectoryService, collectionType); + return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType); } return null; @@ -246,11 +246,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies item = ResolveVideo<Movie>(args, true); } - else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) - { - item = ResolveVideo<Video>(args, false); - } - else if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) || + string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)) { item = ResolveVideo<Video>(args, false); } @@ -482,7 +479,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies return null; } - return new T + var returnVideo = new T { Path = folderPaths[0], @@ -492,9 +489,13 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies Name = result.Stacks[0].Name }; + + SetIsoType(returnVideo); + + return returnVideo; } - private bool IsInvalid(Folder parent, string collectionType, IEnumerable<FileSystemMetadata> files) + private bool IsInvalid(Folder parent, string collectionType) { if (parent != null) { diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 1a873f01e..e62049821 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV // Not officially supported but in some cases we can handle it. if (season == null) { - season = parent.Parents.OfType<Season>().FirstOrDefault(); + season = parent.GetParents().OfType<Season>().FirstOrDefault(); } // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something diff --git a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs index d6aff1192..62392c1c7 100644 --- a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs @@ -156,19 +156,19 @@ namespace MediaBrowser.Server.Implementations.Library } AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name); - - var mediaItems = _libraryManager.GetItems(new InternalItemsQuery + + var mediaItems = _libraryManager.GetItems(new InternalItemsQuery(user) { NameContains = searchTerm, ExcludeItemTypes = excludeItemTypes.ToArray(), IncludeItemTypes = includeItemTypes.ToArray(), - MaxParentalRating = user == null ? null : user.Policy.MaxParentalRating, - Limit = (query.Limit.HasValue ? (int?)(query.Limit.Value * 3) : null), + Limit = query.Limit, + IncludeItemsByName = true - }).Items; + }, new string[] { }); // Add search hints based on item name - hints.AddRange(mediaItems.Where(i => IncludeInSearch(i) && IsVisible(i, user)).Select(item => + hints.AddRange(mediaItems.Where(IncludeInSearch).Select(item => { var index = GetIndex(item.Name, searchTerm, terms); @@ -184,25 +184,6 @@ namespace MediaBrowser.Server.Implementations.Library return Task.FromResult(returnValue); } - private bool IsVisible(BaseItem item, User user) - { - if (user == null) - { - return true; - } - - if (item is IItemByName) - { - var dual = item as IHasDualAccess; - if (dual == null || dual.IsAccessedByName) - { - return true; - } - } - - return item.IsVisibleStandalone(user); - } - private bool IncludeInSearch(BaseItem item) { var episode = item as Episode; diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index cf1853c5e..0d588b97b 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -704,8 +704,7 @@ namespace MediaBrowser.Server.Implementations.Library Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true, - EnableUserViews = true + UsesIdForConfigurationPath = true }; } diff --git a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs index 0df4742bd..a375dde31 100644 --- a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Audio; namespace MediaBrowser.Server.Implementations.Library { @@ -48,103 +49,63 @@ namespace MediaBrowser.Server.Implementations.Library .OfType<Folder>() .ToList(); - var plainFolderIds = user.Configuration.PlainFolderViews.Select(i => new Guid(i)).ToList(); + if (!query.IncludeHidden) + { + folders = folders.Where(i => + { + var hidden = i as IHiddenFromDisplay; + return hidden == null || !hidden.IsHiddenFromUser(user); + }).ToList(); + } - var standaloneFolders = folders - .Where(i => UserView.IsExcludedFromGrouping(i) || !user.IsFolderGrouped(i.Id)) - .ToList(); + var plainFolderIds = user.Configuration.PlainFolderViews.Select(i => new Guid(i)).ToList(); - var foldersWithViewTypes = folders - .Except(standaloneFolders) - .OfType<ICollectionFolder>() - .ToList(); + var groupedFolders = new List<ICollectionFolder>(); var list = new List<Folder>(); - var enableUserViews = _config.Configuration.EnableUserViews || user.EnableUserViews; - - if (enableUserViews) + foreach (var folder in folders) { - foreach (var folder in standaloneFolders) + var collectionFolder = folder as ICollectionFolder; + var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; + + if (UserView.IsUserSpecific(folder)) { - var collectionFolder = folder as ICollectionFolder; - var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; + list.Add(await _libraryManager.GetNamedView(user, folder.Name, folder.Id.ToString("N"), folderViewType, null, cancellationToken).ConfigureAwait(false)); + continue; + } - if (UserView.IsUserSpecific(folder)) - { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - else if (plainFolderIds.Contains(folder.Id)) - { - list.Add(await GetUserView(folder, folderViewType, false, string.Empty, cancellationToken).ConfigureAwait(false)); - } - else if (!string.IsNullOrWhiteSpace(folderViewType)) - { - list.Add(await GetUserView(folder, folderViewType, true, string.Empty, cancellationToken).ConfigureAwait(false)); - } - else - { - list.Add(folder); - } + if (plainFolderIds.Contains(folder.Id) && UserView.IsEligibleForEnhancedView(folderViewType)) + { + list.Add(folder); + continue; } - } - else - { - // TODO: Deprecate this whole block - foreach (var folder in standaloneFolders) + + if (collectionFolder != null && UserView.IsEligibleForGrouping(folder) && user.IsFolderGrouped(folder.Id)) { - var collectionFolder = folder as ICollectionFolder; - var folderViewType = collectionFolder == null ? null : collectionFolder.CollectionType; - - if (UserView.IsUserSpecific(folder)) - { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - else if (plainFolderIds.Contains(folder.Id)) - { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, false, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - else if (!string.IsNullOrWhiteSpace(folderViewType)) - { - list.Add(await GetUserView(folder.Id, folder.Name, folderViewType, true, string.Empty, user, cancellationToken).ConfigureAwait(false)); - } - else - { - list.Add(folder); - } + groupedFolders.Add(collectionFolder); + continue; } - } - - var parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) - .ToList(); - - if (parents.Count > 0) - { - list.Add(await GetUserView(parents, list, CollectionType.TvShows, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); - } - - parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Music, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) - .ToList(); - if (parents.Count > 0) - { - list.Add(await GetUserView(parents, list, CollectionType.Music, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); + if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + list.Add(await GetUserView(folder, folderViewType, string.Empty, cancellationToken).ConfigureAwait(false)); + } + else + { + list.Add(folder); + } } - parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Movies, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.GetViewType(user))) - .ToList(); - - if (parents.Count > 0) + foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) { - list.Add(await GetUserView(parents, list, CollectionType.Movies, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); - } - - parents = foldersWithViewTypes.Where(i => string.Equals(i.GetViewType(user), CollectionType.Games, StringComparison.OrdinalIgnoreCase)) - .ToList(); + var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(i.CollectionType)) + .ToList(); - if (parents.Count > 0) - { - list.Add(await GetUserView(parents, list, CollectionType.Games, string.Empty, user, enableUserViews, cancellationToken).ConfigureAwait(false)); + if (parents.Count > 0) + { + list.Add(await GetUserView(parents, viewType, string.Empty, user, query.PresetViews, cancellationToken).ConfigureAwait(false)); + } } if (user.Configuration.DisplayFoldersView) @@ -187,6 +148,18 @@ namespace MediaBrowser.Server.Implementations.Library { var index = orders.IndexOf(i.Id.ToString("N")); + if (index == -1) + { + var view = i as UserView; + if (view != null) + { + if (view.DisplayParentId != Guid.Empty) + { + index = orders.IndexOf(view.DisplayParentId.ToString("N")); + } + } + } + return index == -1 ? int.MaxValue : index; }) .ThenBy(sorted.IndexOf) @@ -207,32 +180,25 @@ namespace MediaBrowser.Server.Implementations.Library return GetUserSubView(name, parentId, type, sortName, cancellationToken); } - private async Task<UserView> GetUserView(List<ICollectionFolder> parents, List<Folder> currentViews, string viewType, string sortName, User user, bool enableUserViews, CancellationToken cancellationToken) + private async Task<Folder> GetUserView(List<ICollectionFolder> parents, string viewType, string sortName, User user, string[] presetViews, CancellationToken cancellationToken) { - if (parents.Count == 1 && parents.All(i => string.Equals((enableUserViews ? i.GetViewType(user) : i.CollectionType), viewType, StringComparison.OrdinalIgnoreCase))) + if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase))) { - var parentId = parents[0].Id; - - var enableRichView = !user.Configuration.PlainFolderViews.Contains(parentId.ToString("N"), StringComparer.OrdinalIgnoreCase); + if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase)) + { + return (Folder)parents[0]; + } - return await GetUserView((Folder)parents[0], viewType, enableRichView, string.Empty, cancellationToken).ConfigureAwait(false); + return await GetUserView((Folder)parents[0], viewType, string.Empty, cancellationToken).ConfigureAwait(false); } var name = _localizationManager.GetLocalizedString("ViewType" + viewType); return await _libraryManager.GetNamedView(user, name, viewType, sortName, cancellationToken).ConfigureAwait(false); } - public Task<UserView> GetUserView(Guid parentId, string name, string viewType, bool enableRichView, string sortName, User user, CancellationToken cancellationToken) - { - viewType = enableRichView ? viewType : null; - return _libraryManager.GetNamedView(user, name, parentId.ToString("N"), viewType, sortName, null, cancellationToken); - } - - public Task<UserView> GetUserView(Folder parent, string viewType, bool enableRichView, string sortName, CancellationToken cancellationToken) + public Task<UserView> GetUserView(Folder parent, string viewType, string sortName, CancellationToken cancellationToken) { - viewType = enableRichView ? viewType : null; - - return _libraryManager.GetShadowView(parent, viewType, sortName, null, cancellationToken); + return _libraryManager.GetShadowView(parent, viewType, sortName, cancellationToken); } public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request) @@ -243,16 +209,8 @@ namespace MediaBrowser.Server.Implementations.Library var currentUser = user; - Func<BaseItem, bool> filter = i => + var libraryItems = GetItemsForLatestItems(user, request.ParentId, includeTypes, request.Limit ?? 10).Where(i => { - if (includeTypes.Length > 0) - { - if (!includeTypes.Contains(i.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - if (request.IsPlayed.HasValue) { var val = request.IsPlayed.Value; @@ -262,29 +220,12 @@ namespace MediaBrowser.Server.Implementations.Library } } - return i.LocationType != LocationType.Virtual && !i.IsFolder; - }; - - // Avoid implicitly captured closure - var libraryItems = string.IsNullOrEmpty(request.ParentId) && user != null ? - GetItemsConfiguredForLatest(user, filter) : - GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId, filter); - - libraryItems = libraryItems.OrderByDescending(i => i.DateCreated); - - if (request.IsPlayed.HasValue) - { - var takeLimit = (request.Limit ?? 20) * 20; - libraryItems = libraryItems.Take(takeLimit); - } - - // Avoid implicitly captured closure - var items = libraryItems - .ToList(); + return true; + }); var list = new List<Tuple<BaseItem, List<BaseItem>>>(); - foreach (var item in items) + foreach (var item in libraryItems) { // Only grab the index container for media var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer; @@ -316,59 +257,34 @@ namespace MediaBrowser.Server.Implementations.Library return list; } - protected IList<BaseItem> GetAllLibraryItems(string userId, IUserManager userManager, ILibraryManager libraryManager, string parentId, Func<BaseItem, bool> filter) + private IEnumerable<BaseItem> GetItemsForLatestItems(User user, string parentId, string[] includeItemTypes, int limit) { - if (!string.IsNullOrEmpty(parentId)) - { - var folder = (Folder)libraryManager.GetItemById(new Guid(parentId)); - - if (!string.IsNullOrWhiteSpace(userId)) - { - var user = userManager.GetUserById(userId); - - if (user == null) - { - throw new ArgumentException("User not found"); - } - - return folder - .GetRecursiveChildren(user, filter) - .ToList(); - } + var parentIds = string.IsNullOrEmpty(parentId) + ? new string[] { } + : new[] { parentId }; - return folder - .GetRecursiveChildren(filter); - } - if (!string.IsNullOrWhiteSpace(userId)) + if (parentIds.Length == 0) { - var user = userManager.GetUserById(userId); - - if (user == null) - { - throw new ArgumentException("User not found"); - } - - return user - .RootFolder - .GetRecursiveChildren(user, filter) - .ToList(); + parentIds = user.RootFolder.GetChildren(user, true) + .OfType<Folder>() + .Select(i => i.Id.ToString("N")) + .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i)) + .ToArray(); } - return libraryManager - .RootFolder - .GetRecursiveChildren(filter); - } - - private IEnumerable<BaseItem> GetItemsConfiguredForLatest(User user, Func<BaseItem, bool> filter) - { - // Avoid implicitly captured closure - var currentUser = user; + var excludeItemTypes = includeItemTypes.Length == 0 ? new[] { "ChannelItem", "LiveTvItem", typeof(Person).Name, typeof(Studio).Name, typeof(Year).Name, typeof(GameGenre).Name, typeof(MusicGenre).Name, typeof(Genre).Name } : new string[] { }; - return user.RootFolder.GetChildren(user, true) - .OfType<Folder>() - .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) - .SelectMany(i => i.GetRecursiveChildren(currentUser, filter)) - .DistinctBy(i => i.Id); + return _libraryManager.GetItems(new InternalItemsQuery(user) + { + IncludeItemTypes = includeItemTypes, + SortOrder = SortOrder.Descending, + SortBy = new[] { ItemSortBy.DateCreated }, + IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null, + ExcludeItemTypes = excludeItemTypes, + ExcludeLocationTypes = new[] { LocationType.Virtual }, + Limit = limit * 20 + + }, parentIds); } } } diff --git a/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs index 26cde925e..2161c1454 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -165,11 +165,14 @@ namespace MediaBrowser.Server.Implementations.Library.Validators var item = _libraryManager.GetItemById(id); - await _libraryManager.DeleteItem(item, new DeleteOptions + if (item != null) { - DeleteFileLocation = false + await _libraryManager.DeleteItem(item, new DeleteOptions + { + DeleteFileLocation = false - }).ConfigureAwait(false); + }).ConfigureAwait(false); + } } progress.Report(100); diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs new file mode 100644 index 000000000..9ac96165f --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -0,0 +1,50 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class DirectRecorder : IRecorder + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + + public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem) + { + _logger = logger; + _httpClient = httpClient; + _fileSystem = fileSystem; + } + + public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken) + { + var httpRequestOptions = new HttpRequestOptions() + { + Url = mediaSource.Path + }; + + httpRequestOptions.BufferContent = false; + + using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) + { + _logger.Info("Opened recording stream from tuner provider"); + + using (var output = _fileSystem.GetFileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + onStarted(); + + _logger.Info("Copying recording stream to file stream"); + + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index cd91684ce..9e4cb66a8 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -171,7 +171,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { epgData = GetEpgDataForChannel(timer.ChannelId); } - await UpdateTimersForSeriesTimer(epgData, timer, false).ConfigureAwait(false); + await UpdateTimersForSeriesTimer(epgData, timer, true).ConfigureAwait(false); } var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); @@ -664,12 +664,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException("timer"); } + ProgramInfo info = null; + if (string.IsNullOrWhiteSpace(timer.ProgramId)) { - throw new InvalidOperationException("timer.ProgramId is null. Cannot record."); + _logger.Info("Timer {0} has null programId", timer.Id); + } + else + { + info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); } - var info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId); + if (info == null) + { + _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); + info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); + } if (info == null) { @@ -742,7 +752,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV try { - var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None); + var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); var mediaStreamInfo = result.Item1; var isResourceOpen = true; @@ -754,10 +764,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var duration = recordingEndDate - DateTime.UtcNow; - HttpRequestOptions httpRequestOptions = new HttpRequestOptions() + var recorder = await GetRecorder().ConfigureAwait(false); + + if (recorder is EncodedRecorder) { - Url = mediaStreamInfo.Path - }; + recordPath = Path.ChangeExtension(recordPath, ".mp4"); + } recording.Path = recordPath; recording.Status = RecordingStatus.InProgress; @@ -766,21 +778,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); - httpRequestOptions.BufferContent = false; var durationToken = new CancellationTokenSource(duration); var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - httpRequestOptions.CancellationToken = linkedToken; + _logger.Info("Writing file to path: " + recordPath); - using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET")) + _logger.Info("Opening recording stream from tuner provider"); + + Action onStarted = () => { - using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - result.Item2.Release(); - isResourceOpen = false; + result.Item2.Release(); + isResourceOpen = false; + }; - await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken); - } - } + await recorder.Record(mediaStreamInfo, recordPath, onStarted, linkedToken).ConfigureAwait(false); recording.Status = RecordingStatus.Completed; _logger.Info("Recording completed"); @@ -831,6 +841,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + private async Task<IRecorder> GetRecorder() + { + if (GetConfiguration().EnableRecordingEncoding) + { + var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false); + + if (regInfo.IsValid) + { + return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); + } + } + + return new DirectRecorder(_logger, _httpClient, _fileSystem); + } + private async void OnSuccessfulRecording(RecordingInfo recording) { if (GetConfiguration().EnableAutoOrganize) @@ -862,6 +887,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return epgData.FirstOrDefault(p => string.Equals(p.Id, programId, StringComparison.OrdinalIgnoreCase)); } + private ProgramInfo GetProgramInfoFromCache(string channelId, DateTime startDateUtc) + { + var epgData = GetEpgDataForChannel(channelId); + var startDateTicks = startDateUtc.Ticks; + // Find the first program that starts within 3 minutes + return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks); + } + private string RecordingPath { get diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs new file mode 100644 index 000000000..b4ff79567 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class EncodedRecorder : IRecorder + { + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private readonly IApplicationPaths _appPaths; + private bool _hasExited; + private Stream _logFileStream; + private string _targetPath; + private Process _process; + private readonly IJsonSerializer _json; + + public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json) + { + _logger = logger; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + _appPaths = appPaths; + _json = json; + } + + public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken) + { + _targetPath = targetFile; + _fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile)); + + 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, + RedirectStandardInput = true, + + FileName = _mediaEncoder.EncoderPath, + Arguments = GetCommandLineArgs(mediaSource, targetFile), + + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + + EnableRaisingEvents = true + }; + + _process = process; + + var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; + _logger.Info(commandLineLogMessage); + + var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); + _fileSystem.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); + + var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); + await _logFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false); + + process.Exited += (sender, args) => OnFfMpegProcessExited(process); + + process.Start(); + + cancellationToken.Register(Stop); + + // MUST read both stdout and stderr asynchronously or a deadlock may occurr + process.BeginOutputReadLine(); + + // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback + StartStreamingLog(process.StandardError.BaseStream, _logFileStream); + + // Wait for the file to exist before proceeeding + while (!_hasExited) + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + } + + private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile) + { + string videoArgs; + if (EncodeVideo(mediaSource)) + { + var maxBitrate = 25000000; + videoArgs = string.Format( + "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync vfr -profile:v high -level 41", + GetOutputSizeParam(), + maxBitrate.ToString(CultureInfo.InvariantCulture)); + } + else + { + videoArgs = "-codec:v:0 copy"; + } + + var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + + if (mediaSource.ReadAtNativeFramerate) + { + commandLineArgs = "-re " + commandLineArgs; + } + + commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource)); + + return commandLineArgs; + } + + private string GetAudioArgs(MediaSourceInfo mediaSource) + { + var copyAudio = new[] { "aac", "mp3" }; + var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); + if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase))) + { + return "-codec:a:0 copy"; + } + + var audioChannels = 2; + var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + if (audioStream != null) + { + audioChannels = audioStream.Channels ?? audioChannels; + } + return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture); + } + + private bool EncodeVideo(MediaSourceInfo mediaSource) + { + var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); + return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase) && !i.IsInterlaced); + } + + protected string GetOutputSizeParam() + { + var filters = new List<string>(); + + filters.Add("yadif=0:-1:0"); + + var output = string.Empty; + + if (filters.Count > 0) + { + output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + } + + return output; + } + + private void Stop() + { + if (!_hasExited) + { + try + { + _logger.Info("Killing ffmpeg recording process for {0}", _targetPath); + + //process.Kill(); + _process.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous + _process.WaitForExit(5000); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing transcoding job for {0}", ex, _targetPath); + } + } + } + + /// <summary> + /// Processes the exited. + /// </summary> + /// <param name="process">The process.</param> + private void OnFfMpegProcessExited(Process process) + { + _hasExited = true; + + _logger.Debug("Disposing stream resources"); + DisposeLogStream(); + + try + { + _logger.Info("FFMpeg exited with code {0}", process.ExitCode); + } + catch + { + _logger.Error("FFMpeg exited with an error."); + } + } + + private void DisposeLogStream() + { + if (_logFileStream != null) + { + try + { + _logFileStream.Dispose(); + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing log stream", ex); + } + + _logFileStream = null; + } + } + + private async void StartStreamingLog(Stream source, Stream target) + { + try + { + using (var reader = new StreamReader(source)) + { + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync().ConfigureAwait(false); + + var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); + + await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await target.FlushAsync().ConfigureAwait(false); + } + } + } + catch (ObjectDisposedException) + { + // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux + } + catch (Exception ex) + { + _logger.ErrorException("Error reading ffmpeg log", ex); + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs new file mode 100644 index 000000000..12e73c1f3 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public interface IRecorder + { + Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken); + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 8bf1d27b8..14bfcba27 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -300,6 +300,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv public async Task<ILiveTvRecording> GetInternalRecording(string id, CancellationToken cancellationToken) { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + var result = await GetInternalRecordings(new RecordingQuery { Id = id @@ -560,6 +565,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv } item.ExternalId = channelInfo.Id; + if (!item.ParentId.Equals(parentFolderId)) + { + isNew = true; + } + item.ParentId = parentFolderId; + item.ChannelType = channelInfo.ChannelType; item.ServiceName = serviceName; item.Number = channelInfo.Number; @@ -622,6 +633,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } + if (!item.ParentId.Equals(channel.Id)) + { + forceUpdate = true; + } + item.ParentId = channel.Id; + //item.ChannelType = channelType; if (!string.Equals(item.ServiceName, serviceName, StringComparison.Ordinal)) { @@ -774,6 +791,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv } recording.IsSeries = info.IsSeries; + if (!item.ParentId.Equals(parentFolderId)) + { + dataChanged = true; + } + item.ParentId = parentFolderId; + if (!item.HasImage(ImageType.Primary)) { if (!string.IsNullOrWhiteSpace(info.ImagePath)) @@ -851,7 +874,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, MinEndDate = query.MinEndDate, @@ -869,16 +892,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv SortOrder = query.SortOrder ?? SortOrder.Ascending }; - if (user != null) - { - internalQuery.MaxParentalRating = user.Policy.MaxParentalRating; - - if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram)) - { - internalQuery.HasParentalRating = true; - } - } - if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -913,7 +926,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = _userManager.GetUserById(query.UserId); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, @@ -922,16 +935,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv IsKids = query.IsKids }; - if (user != null) - { - internalQuery.MaxParentalRating = user.Policy.MaxParentalRating; - - if (user.Policy.BlockUnratedItems.Contains(UnratedItem.LiveTvProgram)) - { - internalQuery.HasParentalRating = true; - } - } - if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -1399,7 +1402,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv await RefreshRecordings(cancellationToken).ConfigureAwait(false); - var internalQuery = new InternalItemsQuery + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvVideoRecording).Name, typeof(LiveTvAudioRecording).Name } }; @@ -1409,10 +1412,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv internalQuery.ChannelIds = new[] { query.ChannelId }; } - var queryResult = _libraryManager.GetItems(internalQuery); - IEnumerable<ILiveTvRecording> recordings = queryResult.Items.Cast<ILiveTvRecording>(); + var queryResult = _libraryManager.GetItems(internalQuery, new string[] { }); + IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>(); - if (!string.IsNullOrEmpty(query.Id)) + if (!string.IsNullOrWhiteSpace(query.Id)) { var guid = new Guid(query.Id); @@ -1420,7 +1423,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv .Where(i => i.Id == guid); } - if (!string.IsNullOrEmpty(query.GroupId)) + if (!string.IsNullOrWhiteSpace(query.GroupId)) { var guid = new Guid(query.GroupId); @@ -1469,7 +1472,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv }; } - public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, bool addChannelInfo, User user = null) + public void AddInfoToProgramDto(BaseItem item, BaseItemDto dto, List<ItemFields> fields, User user = null) { var program = (LiveTvProgram)item; @@ -1509,13 +1512,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv dto.IsPremiere = program.IsPremiere; } - if (addChannelInfo) + if (fields.Contains(ItemFields.ChannelInfo)) { var channel = GetInternalChannel(program.ChannelId); if (channel != null) { dto.ChannelName = channel.Name; + dto.MediaType = channel.MediaType; if (channel.HasImage(ImageType.Primary)) { @@ -1523,6 +1527,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } } + + if (fields.Contains(ItemFields.ServiceName)) + { + var service = GetService(program); + if (service != null) + { + dto.ServiceName = service.Name; + } + } } public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, User user = null) @@ -1593,18 +1606,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv var internalResult = await GetInternalRecordings(query, cancellationToken).ConfigureAwait(false); - var returnArray = internalResult.Items - .Select(i => _dtoService.GetBaseItemDto(i, options, user)) + var tuples = internalResult.Items + .Select(i => new Tuple<BaseItem, BaseItemDto>(i, _dtoService.GetBaseItemDto(i, options, user))) .ToArray(); if (user != null) { - _dtoService.FillSyncInfo(returnArray, new DtoOptions(), user); + _dtoService.FillSyncInfo(tuples, new DtoOptions(), user); } return new QueryResult<BaseItemDto> { - Items = returnArray, + Items = tuples.Select(i => i.Item2).ToArray(), TotalRecordCount = internalResult.TotalRecordCount }; } @@ -1674,6 +1687,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv throw new ResourceNotFoundException(string.Format("Recording with Id {0} not found", recordingId)); } + await DeleteRecording(recording).ConfigureAwait(false); + } + + public async Task DeleteRecording(ILiveTvRecording recording) + { var service = GetService(recording.ServiceName); try @@ -1685,6 +1703,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv } + _lastRecordingRefreshTime = DateTime.MinValue; + // This is the responsibility of the live tv service await _libraryManager.DeleteItem((BaseItem)recording, new DeleteOptions { @@ -1812,7 +1832,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var now = DateTime.UtcNow; - var programs = _libraryManager.GetItems(new InternalItemsQuery + var programs = _libraryManager.GetItems(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, ChannelIds = new[] { id }, @@ -1821,7 +1841,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv Limit = 1, SortBy = new[] { "StartDate" } - }).Items.Cast<LiveTvProgram>(); + }, new string[] { }).Cast<LiveTvProgram>(); var currentProgram = programs.FirstOrDefault(); @@ -1836,7 +1856,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var now = DateTime.UtcNow; - var programs = _libraryManager.GetItems(new InternalItemsQuery + var programs = _libraryManager.GetItems(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, ChannelIds = new[] { channel.Id.ToString("N") }, @@ -1845,7 +1865,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv Limit = 1, SortBy = new[] { "StartDate" } - }).Items.Cast<LiveTvProgram>(); + }, new string[] { }).Cast<LiveTvProgram>(); var currentProgram = programs.FirstOrDefault(); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index ddbbb030d..f87d4f43f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -46,53 +46,59 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) { - var url = info.Url; - var urlHash = url.GetMD5().ToString("N"); + var urlHash = info.Url.GetMD5().ToString("N"); - string line; // Read the file and display it line by line. - using (var file = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) + using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) { - var channels = new List<M3UChannel>(); + return GetChannels(reader, urlHash); + } + } + + private List<M3UChannel> GetChannels(StreamReader reader, string urlHash) + { + var channels = new List<M3UChannel>(); - string channnelName = null; - string channelNumber = null; + string channnelName = null; + string channelNumber = null; + string line; - while ((line = file.ReadLine()) != null) + while ((line = reader.ReadLine()) != null) + { + line = line.Trim(); + if (string.IsNullOrWhiteSpace(line)) { - line = line.Trim(); - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } + continue; + } - if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) - { - continue; - } + if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) + { + continue; + } - if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) - { - var parts = line.Split(new[] { ':' }, 2).Last().Split(new[] { ',' }, 2); - channelNumber = parts[0]; - channnelName = parts[1]; - } - else if (!string.IsNullOrWhiteSpace(channelNumber)) + if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase)) + { + line = line.Substring(8); + Logger.Info("Found m3u channel: {0}", line); + var parts = line.Split(new[] { ',' }, 2); + channelNumber = parts[0]; + channnelName = parts[1]; + } + else if (!string.IsNullOrWhiteSpace(channelNumber)) + { + channels.Add(new M3UChannel { - channels.Add(new M3UChannel - { - Name = channnelName, - Number = channelNumber, - Id = ChannelIdPrefix + urlHash + channelNumber, - Path = line - }); - - channelNumber = null; - channnelName = null; - } + Name = channnelName, + Number = channelNumber, + Id = ChannelIdPrefix + urlHash + line.GetMD5().ToString("N"), + Path = line + }); + + channelNumber = null; + channnelName = null; } - return channels; } + return channels; } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -159,8 +165,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts return null; } - //channelId = channelId.Substring(prefix.Length); - var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false); var m3uchannels = channels.Cast<M3UChannel>(); var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase)); diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs deleted file mode 100644 index ecd2864c5..000000000 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts -{ - public class SatIp : BaseTunerHost - { - public SatIp(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) - : base(config, logger, jsonSerializer, mediaEncoder) - { - } - - protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override string Type - { - get { return "SatIp"; } - } - - protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected override bool IsValidChannelId(string channelId) - { - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs new file mode 100644 index 000000000..95c04d61f --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class SatIpDiscovery : IServerEntryPoint + { + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly ILiveTvManager _liveTvManager; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private readonly IHttpClient _httpClient; + + public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient) + { + _deviceDiscovery = deviceDiscovery; + _config = config; + _logger = logger; + _liveTvManager = liveTvManager; + _httpClient = httpClient; + } + + public void Run() + { + _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + } + + void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e) + { + //string server = null; + //if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1) + //{ + // string location; + // if (e.Headers.TryGetValue("Location", out location)) + // { + // //_logger.Debug("HdHomerun found at {0}", location); + + // // Just get the beginning of the url + // Uri uri; + // if (Uri.TryCreate(location, UriKind.Absolute, out uri)) + // { + // var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase) + // .TrimEnd('/'); + + // //_logger.Debug("HdHomerun api url: {0}", apiUrl); + // AddDevice(apiUrl); + // } + // } + //} + } + + private async void AddDevice(string url) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + var options = GetConfiguration(); + + if (options.TunerHosts.Any(i => + string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && + UriEquals(i.Url, url))) + { + return; + } + + // Strip off the port + url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/'); + + await TestUrl(url).ConfigureAwait(false); + + await _liveTvManager.SaveTunerHost(new TunerHostInfo + { + Type = SatIpHost.DeviceType, + Url = url + + }).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving device", ex); + } + finally + { + _semaphore.Release(); + } + } + + private async Task TestUrl(string url) + { + // Test it by pulling down the lineup + using (await _httpClient.Get(new HttpRequestOptions + { + Url = string.Format("{0}/lineup.json", url), + CancellationToken = CancellationToken.None + })) + { + } + } + + private bool UriEquals(string savedUri, string location) + { + return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase); + } + + private string NormalizeUrl(string url) + { + if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + url = "http://" + url; + } + + url = url.TrimEnd('/'); + + // Strip off the port + return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped); + } + + private LiveTvOptions GetConfiguration() + { + return _config.GetConfiguration<LiveTvOptions>("livetv"); + } + + public void Dispose() + { + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs new file mode 100644 index 000000000..205cdf74e --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -0,0 +1,45 @@ +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class SatIpHost /*: BaseTunerHost*/ + { + //public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) + // : base(config, logger, jsonSerializer, mediaEncoder) + //{ + //} + + //protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + public static string DeviceType + { + get { return "satip"; } + } + + //public override string Type + //{ + // get { return DeviceType; } + //} + + //protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) + //{ + // throw new NotImplementedException(); + //} + + //protected override bool IsValidChannelId(string channelId) + //{ + // throw new NotImplementedException(); + //} + } +} diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ar.json b/MediaBrowser.Server.Implementations/Localization/Core/ar.json index b6b07dc1c..28977c4f9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ar.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ar.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json b/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json index aeebd2038..22b99408d 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\u0421\u043c\u0435\u0441\u0435\u043d\u043e \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ca.json b/MediaBrowser.Server.Implementations/Localization/Core/ca.json index 83d72030f..7cec213d3 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ca.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ca.json @@ -1,10 +1,11 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Dispositiu: {1}", "UserDownloadingItemWithValues": "{0} est\u00e0 descarregant {1}", - "FolderTypeMixed": "Mixed content", + "FolderTypeMixed": "Contingut barrejat", "FolderTypeMovies": "Pel\u00b7l\u00edcules", "FolderTypeMusic": "M\u00fasica", - "FolderTypeAdultVideos": "Adult videos", + "FolderTypeAdultVideos": "V\u00eddeos per adults", "FolderTypePhotos": "Fotos", "FolderTypeMusicVideos": "V\u00eddeos musicals", "FolderTypeHomeVideos": "V\u00eddeos dom\u00e8stics", @@ -115,7 +116,7 @@ "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "AuthenticationSucceededWithUserName": "{0} autenticat correctament", "DeviceOfflineWithName": "{0} has disconnected", - "UserLockedOutWithName": "User {0} has been locked out", + "UserLockedOutWithName": "L'usuari {0} ha estat blocat", "UserOfflineFromDevice": "{0} has disconnected from {1}", "UserStartedPlayingItemWithValues": "{0} ha comen\u00e7at a reproduir {1}", "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}", @@ -157,13 +158,13 @@ "HeaderVideo": "V\u00eddeo", "HeaderEmbeddedImage": "Embedded image", "HeaderResolution": "Resolution", - "HeaderSubtitles": "Subtitles", + "HeaderSubtitles": "Subt\u00edtols", "HeaderGenres": "Genres", "HeaderCountries": "Countries", "HeaderStatus": "Estat", "HeaderTracks": "Tracks", "HeaderMusicArtist": "M\u00fasic", - "HeaderLocked": "Locked", + "HeaderLocked": "Blocat", "HeaderStudios": "Estudis", "HeaderActor": "Actors", "HeaderComposer": "Compositors", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/core.json b/MediaBrowser.Server.Implementations/Localization/Core/core.json index 5f11b9436..976faa8cb 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/core.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/core.json @@ -174,5 +174,6 @@ "HeaderWriter": "Writers", "HeaderParentalRatings": "Parental Ratings", "HeaderCommunityRatings": "Community ratings", - "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly.", + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete." } diff --git a/MediaBrowser.Server.Implementations/Localization/Core/cs.json b/MediaBrowser.Server.Implementations/Localization/Core/cs.json index 3d630abb4..9c62daf73 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/cs.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/cs.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "Aplikace: {0}, Za\u0159\u00edzen\u00ed: {1}", "UserDownloadingItemWithValues": "{0} pr\u00e1v\u011b stahuje {1}", "FolderTypeMixed": "Sm\u00ed\u0161en\u00fd obsah", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/da.json b/MediaBrowser.Server.Implementations/Localization/Core/da.json index e6e4d9835..d2a628a80 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/da.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/da.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Enhed: {1}", "UserDownloadingItemWithValues": "{0} henter {1}", "FolderTypeMixed": "Blandet indhold", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/de.json b/MediaBrowser.Server.Implementations/Localization/Core/de.json index 0f22ff479..30e3d9215 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/de.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/de.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Bitte warten Sie w\u00e4hrend die Emby Datenbank aktualisiert wird. {0}% verarbeitet.", "AppDeviceValues": "App: {0}, Ger\u00e4t: {1}", "UserDownloadingItemWithValues": "{0} l\u00e4dt {1} herunter", "FolderTypeMixed": "Gemischte Inhalte", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/el.json b/MediaBrowser.Server.Implementations/Localization/Core/el.json index 7e94f7987..9e2d321cc 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/el.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/el.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\u0391\u03bd\u03ac\u03bc\u03b5\u03b9\u03ba\u03c4\u03bf \u03a0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json b/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json index 4462c00fd..493c6c4e9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-GB.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json index e444c0e93..0d5b5c4aa 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json b/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json index 8068e32a9..0555aa9d9 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es-AR.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json b/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json index 27aeee097..630c7a037 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es-MX.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Por favor espere mientras la base de datos de su Servidor Emby es actualizada. {0}% completo.", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "UserDownloadingItemWithValues": "{0} esta descargando {1}", "FolderTypeMixed": "Contenido mezclado", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es.json b/MediaBrowser.Server.Implementations/Localization/Core/es.json index 60dbd1a44..2a715a5b3 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Contenido mezclado", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/fi.json b/MediaBrowser.Server.Implementations/Localization/Core/fi.json index f0ab5b0d0..20efa1406 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/fi.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/fi.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/fr.json b/MediaBrowser.Server.Implementations/Localization/Core/fr.json index 182e89bf6..25c722989 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/fr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/fr.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Veuillez patienter pendant que la base de donn\u00e9e de votre Emby Serveur se met \u00e0 jour. Termin\u00e9e \u00e0 {0}%.", "AppDeviceValues": "Application : {0}, Appareil: {1}", "UserDownloadingItemWithValues": "{0} est en train de t\u00e9l\u00e9charger {1}", "FolderTypeMixed": "Contenus m\u00e9lang\u00e9s", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/gsw.json b/MediaBrowser.Server.Implementations/Localization/Core/gsw.json index 736c3d18f..88af82b7e 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/gsw.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/gsw.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Verschiedeni Sache", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/he.json b/MediaBrowser.Server.Implementations/Localization/Core/he.json index f900946f1..137b45544 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/he.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/he.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\u05ea\u05d5\u05db\u05df \u05de\u05e2\u05d5\u05e8\u05d1", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/hr.json b/MediaBrowser.Server.Implementations/Localization/Core/hr.json index 2b9d56566..7a94dc32b 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/hr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/hr.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/hu.json b/MediaBrowser.Server.Implementations/Localization/Core/hu.json index 2dc90fb7e..0c9d16d38 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/hu.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/hu.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/id.json b/MediaBrowser.Server.Implementations/Localization/Core/id.json index 4fb839fe8..8d64b63c4 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/id.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/id.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Silahkan menunggu sementara database Emby Server anda diupgrade. {0}% selesai.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/it.json b/MediaBrowser.Server.Implementations/Localization/Core/it.json index 2030ea110..d2d697c3e 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/it.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/it.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "UserDownloadingItemWithValues": "{0} sta scaricando {1}", "FolderTypeMixed": "contenuto misto", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/kk.json b/MediaBrowser.Server.Implementations/Localization/Core/kk.json index be7c4c56e..93252c30b 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/kk.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/kk.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Emby Server \u0434\u0435\u0440\u0435\u043a\u049b\u043e\u0440\u044b\u04a3\u044b\u0437\u0434\u044b\u04a3 \u0436\u0430\u04a3\u0493\u044b\u0440\u0442\u044b\u043b\u0443\u044b\u043d \u043a\u04af\u0442\u0435 \u0442\u04b1\u0440\u044b\u04a3\u044b\u0437. {0} % \u0430\u044f\u049b\u0442\u0430\u043b\u0434\u044b.", "AppDeviceValues": "\u049a\u043e\u043b\u0434\u0430\u043d\u0431\u0430: {0}, \u049a\u04b1\u0440\u044b\u043b\u0493\u044b: {1}", "UserDownloadingItemWithValues": "{0} \u043c\u044b\u043d\u0430\u043d\u044b \u0436\u04af\u043a\u0442\u0435\u043f \u0430\u043b\u0443\u0434\u0430: {1}", "FolderTypeMixed": "\u0410\u0440\u0430\u043b\u0430\u0441 \u043c\u0430\u0437\u043c\u04b1\u043d", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ko.json b/MediaBrowser.Server.Implementations/Localization/Core/ko.json index dcd4d2346..834ccc17b 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ko.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ko.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "\uc571: {0}, \uc7a5\uce58: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\ud63c\ud569 \ucf58\ud150\ud2b8", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ms.json b/MediaBrowser.Server.Implementations/Localization/Core/ms.json index 6b5c4393f..fe5eef894 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ms.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ms.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/nb.json b/MediaBrowser.Server.Implementations/Localization/Core/nb.json index f4ebe89b6..315d49b5f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/nb.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/nb.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0} , Device: {1}", "UserDownloadingItemWithValues": "{0} laster ned {1}", "FolderTypeMixed": "Forskjellig innhold", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/nl.json b/MediaBrowser.Server.Implementations/Localization/Core/nl.json index fd0c586a2..a83182ee8 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/nl.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/nl.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Even geduld svp terwijl de Emby Server database ge-upgrade wordt. {0}% gereed.", "AppDeviceValues": "App: {0}, Apparaat: {1}", "UserDownloadingItemWithValues": "{0} download {1}", "FolderTypeMixed": "Gemengde inhoud", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pl.json b/MediaBrowser.Server.Implementations/Localization/Core/pl.json index 3b7d56def..db22dc6e4 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pl.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pl.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "Aplikacja: {0}, Urz\u0105dzenie: {1}", "UserDownloadingItemWithValues": "{0} pobiera {1}", "FolderTypeMixed": "Zawarto\u015b\u0107 mieszana", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json b/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json index 9548262c9..b714fd44c 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Por favor, aguarde enquanto a base de dados do Servidor Emby \u00e9 atualizada. {0}% completado.", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "UserDownloadingItemWithValues": "{0} est\u00e1 fazendo download de {1}", "FolderTypeMixed": "Conte\u00fado misto", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json b/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json index c3d514c8a..f12939b10 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ro.json b/MediaBrowser.Server.Implementations/Localization/Core/ro.json index a6aa9577b..c58df27d5 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ro.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ro.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Continut mixt", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ru.json b/MediaBrowser.Server.Implementations/Localization/Core/ru.json index c22cf9f84..0aacc5362 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ru.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ru.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0430\u0448\u0435\u043c Emby Server \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044f. {0} % \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e.", "AppDeviceValues": "\u041f\u0440\u0438\u043b.: {0}, \u0423\u0441\u0442\u0440.: {1}", "UserDownloadingItemWithValues": "{0} \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 {1}", "FolderTypeMixed": "\u0420\u0430\u0437\u043d\u043e\u0442\u0438\u043f\u043d\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json b/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json index e444c0e93..0d5b5c4aa 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/sv.json b/MediaBrowser.Server.Implementations/Localization/Core/sv.json index 667b8e4b4..f52f656d4 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/sv.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/sv.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, enhet: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Blandat inneh\u00e5ll", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/tr.json b/MediaBrowser.Server.Implementations/Localization/Core/tr.json index 810df2665..a691e9d02 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/tr.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/tr.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/uk.json b/MediaBrowser.Server.Implementations/Localization/Core/uk.json index 73c9b26f9..0dc6afe8a 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/uk.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/uk.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/vi.json b/MediaBrowser.Server.Implementations/Localization/Core/vi.json index 75a5dbea7..6ea1d1d3f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/vi.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/vi.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json index a9430c0c2..580832a9e 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App\uff1a {0}\uff0c\u8bbe\u5907\uff1a {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\u6df7\u5408\u5185\u5bb9", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json index 8d25a28c4..a70e7a003 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "\u6df7\u5408\u5167\u5bb9", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json index 2bcb5c132..7f289fc1f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json @@ -1,4 +1,5 @@ { + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index f3376aff0..2dee8a5ed 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -45,12 +45,15 @@ <SpecificVersion>False</SpecificVersion> <HintPath>..\packages\CommonIO.1.0.0.7\lib\net45\CommonIO.dll</HintPath> </Reference> + <Reference Include="Emby.XmlTv"> + <HintPath>..\packages\Emby.XmlTv.1.0.0.46\lib\net45\Emby.XmlTv.dll</HintPath> + </Reference> <Reference Include="Interfaces.IO"> <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> </Reference> - <Reference Include="MediaBrowser.Naming, Version=1.0.5877.22335, Culture=neutral, processorArchitecture=MSIL"> + <Reference Include="MediaBrowser.Naming, Version=1.0.5884.23751, Culture=neutral, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\MediaBrowser.Naming.1.0.0.46\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> + <HintPath>..\packages\MediaBrowser.Naming.1.0.0.47\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> </Reference> <Reference Include="MoreLinq"> <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath> @@ -212,8 +215,11 @@ <Compile Include="Library\Validators\StudiosValidator.cs" /> <Compile Include="Library\Validators\YearsPostScanTask.cs" /> <Compile Include="LiveTv\ChannelImageProvider.cs" /> + <Compile Include="LiveTv\EmbyTV\DirectRecorder.cs" /> <Compile Include="LiveTv\EmbyTV\EmbyTV.cs" /> + <Compile Include="LiveTv\EmbyTV\EncodedRecorder.cs" /> <Compile Include="LiveTv\EmbyTV\EntryPoint.cs" /> + <Compile Include="LiveTv\EmbyTV\IRecorder.cs" /> <Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" /> <Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" /> <Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" /> @@ -231,6 +237,8 @@ <Compile Include="LiveTv\ProgramImageProvider.cs" /> <Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\SatIpHost.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\SatIpDiscovery.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> <Compile Include="Logging\PatternsLogger.cs" /> <Compile Include="MediaEncoder\EncodingManager.cs" /> diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index bd634ed73..ac0f2c69d 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -124,8 +124,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder { if (extractImages) { - if (video.VideoType == VideoType.HdDvd || video.VideoType == VideoType.Iso || - video.VideoType == VideoType.BluRay) + if (video.VideoType == VideoType.HdDvd || video.VideoType == VideoType.Iso || video.VideoType == VideoType.BluRay) { continue; } diff --git a/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs index f883e6dbc..7302431e1 100644 --- a/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ b/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs @@ -13,20 +13,11 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Notifications { - public class SqliteNotificationsRepository : INotificationsRepository + public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository { private IDbConnection _connection; - private readonly ILogger _logger; private readonly IServerApplicationPaths _appPaths; - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - - public SqliteNotificationsRepository(ILogManager logManager, IServerApplicationPaths appPaths) - { - _appPaths = appPaths; - _logger = logManager.GetLogger(GetType().Name); - } - public event EventHandler<NotificationUpdateEventArgs> NotificationAdded; public event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead; public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated; @@ -35,11 +26,17 @@ namespace MediaBrowser.Server.Implementations.Notifications private IDbCommand _markReadCommand; private IDbCommand _markAllReadCommand; + public SqliteNotificationsRepository(ILogManager logManager, IServerApplicationPaths appPaths) + : base(logManager) + { + _appPaths = appPaths; + } + public async Task Initialize() { var dbFile = Path.Combine(_appPaths.DataPath, "notifications.db"); - _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); string[] queries = { @@ -52,7 +49,7 @@ namespace MediaBrowser.Server.Implementations.Notifications "pragma shrink_memory" }; - _connection.RunQueries(queries, _logger); + _connection.RunQueries(queries, Logger); PrepareStatements(); } @@ -251,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.Notifications } catch (Exception ex) { - _logger.ErrorException("Error in NotificationAdded event handler", ex); + Logger.ErrorException("Error in NotificationAdded event handler", ex); } } } @@ -275,7 +272,7 @@ namespace MediaBrowser.Server.Implementations.Notifications cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -311,7 +308,7 @@ namespace MediaBrowser.Server.Implementations.Notifications } catch (Exception e) { - _logger.ErrorException("Failed to save notification:", e); + Logger.ErrorException("Failed to save notification:", e); if (transaction != null) { @@ -327,7 +324,7 @@ namespace MediaBrowser.Server.Implementations.Notifications transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -359,7 +356,7 @@ namespace MediaBrowser.Server.Implementations.Notifications } catch (Exception ex) { - _logger.ErrorException("Error in NotificationsMarkedRead event handler", ex); + Logger.ErrorException("Error in NotificationsMarkedRead event handler", ex); } } } @@ -368,7 +365,7 @@ namespace MediaBrowser.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -396,7 +393,7 @@ namespace MediaBrowser.Server.Implementations.Notifications } catch (Exception e) { - _logger.ErrorException("Failed to save notification:", e); + Logger.ErrorException("Failed to save notification:", e); if (transaction != null) { @@ -412,7 +409,7 @@ namespace MediaBrowser.Server.Implementations.Notifications transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -420,7 +417,7 @@ namespace MediaBrowser.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -455,7 +452,7 @@ namespace MediaBrowser.Server.Implementations.Notifications } catch (Exception e) { - _logger.ErrorException("Failed to save notification:", e); + Logger.ErrorException("Failed to save notification:", e); if (transaction != null) { @@ -471,7 +468,21 @@ namespace MediaBrowser.Server.Implementations.Notifications transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); + } + } + + protected override void CloseConnection() + { + if (_connection != null) + { + if (_connection.IsOpen()) + { + _connection.Close(); + } + + _connection.Dispose(); + _connection = null; } } } diff --git a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs index cac112b6c..395907844 100644 --- a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs @@ -1,6 +1,8 @@ using MediaBrowser.Model.Logging; using System; +using System.Data; using System.Threading; +using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { @@ -14,12 +16,48 @@ namespace MediaBrowser.Server.Implementations.Persistence Logger = logManager.GetLogger(GetType().Name); } + private bool _disposed; + protected void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); + } + } + public void Dispose() { + _disposed = true; Dispose(true); GC.SuppressFinalize(this); } + protected async Task Vacuum(IDbConnection connection) + { + CheckDisposed(); + + await WriteLock.WaitAsync().ConfigureAwait(false); + + try + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "vacuum"; + cmd.ExecuteNonQuery(); + } + } + catch (Exception e) + { + Logger.ErrorException("Failed to vacuum:", e); + + throw; + } + finally + { + WriteLock.Release(); + } + } + private readonly object _disposeLock = new object(); /// <summary> diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs index 070c5239e..6ca1bae6d 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs @@ -9,10 +9,16 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Localization; +using MediaBrowser.Controller.Net; +using MediaBrowser.Server.Implementations.ScheduledTasks; namespace MediaBrowser.Server.Implementations.Persistence { @@ -23,14 +29,23 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; + private readonly IHttpServer _httpServer; + private readonly ILocalizationManager _localization; + private readonly ITaskManager _taskManager; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem) + public const int MigrationVersion = 20; + public static bool EnableUnavailableMessage = false; + + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; _config = config; _fileSystem = fileSystem; + _httpServer = httpServer; + _localization = localization; + _taskManager = taskManager; } public string Name @@ -50,30 +65,82 @@ namespace MediaBrowser.Server.Implementations.Persistence public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { + OnProgress(0); + + // Ensure these objects are lazy loaded. + // Without this there is a deadlock that will need to be investigated + var rootChildren = _libraryManager.RootFolder.Children.ToList(); + rootChildren = _libraryManager.GetUserRootFolder().Children.ToList(); + var innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(.4 * p)); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = .4 * p; + OnProgress(newPercentCommplete); + + progress.Report(newPercentCommplete); + }); await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(40 + (.05 * p))); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = 40 + (.05 * p); + OnProgress(newPercentCommplete); + progress.Report(newPercentCommplete); + }); await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(45); innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => progress.Report(45 + (.55 * p))); + innerProgress.RegisterAction(p => + { + double newPercentCommplete = 45 + (.55 * p); + OnProgress(newPercentCommplete); + progress.Report(newPercentCommplete); + }); await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); + + await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); + + if (_config.Configuration.MigrationVersion < MigrationVersion) + { + _config.Configuration.MigrationVersion = MigrationVersion; + _config.SaveConfiguration(); + } + + if (EnableUnavailableMessage) + { + EnableUnavailableMessage = false; + _httpServer.GlobalResponse = null; + _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>(); + } + + _taskManager.SuspendTriggers = false; + } + + private void OnProgress(double newPercentCommplete) + { + if (EnableUnavailableMessage) + { + var html = "<!doctype html><html><head><title>Emby</title></head><body>"; + var text = _localization.GetLocalizedString("DbUpgradeMessage"); + html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture)); + + html += "<script>setTimeout(function(){window.location.reload(true);}, 5000);</script>"; + html += "</body></html>"; + + _httpServer.GlobalResponse = html; + } } private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress<double> progress) { var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - IsCurrentSchema = false, - - // These are constantly getting regenerated so don't bother with them here - ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } + IsCurrentSchema = false }); var numComplete = 0; @@ -85,27 +152,25 @@ namespace MediaBrowser.Server.Implementations.Persistence { cancellationToken.ThrowIfCancellationRequested(); - if (itemId == Guid.Empty) + if (itemId != Guid.Empty) { // Somehow some invalid data got into the db. It probably predates the boundary checking - continue; - } - - var item = _libraryManager.GetItemById(itemId); + var item = _libraryManager.GetItemById(itemId); - if (item != null) - { - try - { - await _itemRepo.SaveItem(item, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) + if (item != null) { - _logger.ErrorException("Error saving item", ex); + try + { + await _itemRepo.SaveItem(item, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error saving item", ex); + } } } @@ -115,12 +180,6 @@ namespace MediaBrowser.Server.Implementations.Persistence progress.Report(percent * 100); } - if (!_config.Configuration.DisableStartupScan) - { - _config.Configuration.DisableStartupScan = true; - _config.SaveConfiguration(); - } - progress.Report(100); } @@ -146,10 +205,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { _logger.Info("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - await _libraryManager.DeleteItem(item, new DeleteOptions + await item.Delete(new DeleteOptions { DeleteFileLocation = false - }); + + }).ConfigureAwait(false); } numComplete++; @@ -165,12 +225,22 @@ namespace MediaBrowser.Server.Implementations.Persistence { var result = _itemRepo.GetItemIdsWithPath(new InternalItemsQuery { - IsOffline = false, LocationType = LocationType.FileSystem, //Limit = limit, // These have their own cleanup routines - ExcludeItemTypes = new[] { typeof(Person).Name, typeof(Genre).Name, typeof(MusicGenre).Name, typeof(GameGenre).Name, typeof(Studio).Name, typeof(Year).Name } + ExcludeItemTypes = new[] + { + typeof(Person).Name, + typeof(Genre).Name, + typeof(MusicGenre).Name, + typeof(GameGenre).Name, + typeof(Studio).Name, + typeof(Year).Name, + typeof(Channel).Name, + typeof(AggregateFolder).Name, + typeof(CollectionFolder).Name + } }); var numComplete = 0; @@ -191,6 +261,11 @@ namespace MediaBrowser.Server.Implementations.Persistence var libraryItem = _libraryManager.GetItemById(item.Item1); + if (libraryItem.IsTopParent) + { + continue; + } + if (Folder.IsPathOffline(path)) { libraryItem.IsOffline = true; @@ -200,10 +275,11 @@ namespace MediaBrowser.Server.Implementations.Persistence _logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItem.Path ?? string.Empty); - await _libraryManager.DeleteItem(libraryItem, new DeleteOptions + await libraryItem.Delete(new DeleteOptions { DeleteFileLocation = false - }); + + }).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -229,4 +305,4 @@ namespace MediaBrowser.Server.Implementations.Persistence }; } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs index f54e702d6..7e46db5a7 100644 --- a/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs +++ b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs @@ -27,6 +27,70 @@ namespace MediaBrowser.Server.Implementations.Persistence AddIsCabacColumn(); AddKeyFramesColumn(); AddRefFramesCommand(); + AddCodecTagColumn(); + AddCommentColumn(); + } + + private void AddCommentColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "Comment", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column Comment TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCodecTagColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "CodecTag", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column CodecTag TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); } private void AddPixelFormatColumnCommand() diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index dc6241362..346dcf359 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -19,18 +19,17 @@ using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Channels; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.Persistence { /// <summary> /// Class SQLiteItemRepository /// </summary> - public class SqliteItemRepository : IItemRepository + public class SqliteItemRepository : BaseSqliteRepository, IItemRepository { private IDbConnection _connection; - private readonly ILogger _logger; - private readonly TypeMapper _typeMapper = new TypeMapper(); /// <summary> @@ -76,7 +75,12 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _deleteStreamsCommand; private IDbCommand _saveStreamCommand; - private const int LatestSchemaVersion = 13; + private IDbCommand _deleteAncestorsCommand; + private IDbCommand _saveAncestorCommand; + + private IDbCommand _updateInheritedRatingCommand; + + private const int LatestSchemaVersion = 45; /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. @@ -90,6 +94,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// jsonSerializer /// </exception> public SqliteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) + : base(logManager) { if (appPaths == null) { @@ -104,8 +109,6 @@ namespace MediaBrowser.Server.Implementations.Persistence _jsonSerializer = jsonSerializer; _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); - - _logger = logManager.GetLogger(GetType().Name); } private const string ChaptersTableName = "Chapters2"; @@ -118,20 +121,27 @@ namespace MediaBrowser.Server.Implementations.Persistence { var dbFile = Path.Combine(_appPaths.DataPath, "library.db"); - _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); var createMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, KeyFrames TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, CodecTag TEXT NULL, Comment TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; string[] queries = { - "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB)", + "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID)", "create index if not exists idx_TypedBaseItems on TypedBaseItems(guid)", + "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", + "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))", + "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", + "create index if not exists idx_AncestorIds2 on AncestorIds(AncestorIdText)", + "create table if not exists ChildrenIds (ParentId GUID, ItemId GUID, PRIMARY KEY (ParentId, ItemId))", "create index if not exists idx_ChildrenIds on ChildrenIds(ParentId,ItemId)", "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", + "create index if not exists idxPeopleItemId on People(ItemId)", + "create index if not exists idxPeopleName on People(Name)", "create table if not exists "+ChaptersTableName+" (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", "create index if not exists idx_"+ChaptersTableName+" on "+ChaptersTableName+"(ItemId, ChapterIndex)", @@ -145,62 +155,77 @@ namespace MediaBrowser.Server.Implementations.Persistence "pragma shrink_memory" }; - _connection.RunQueries(queries, _logger); - - _connection.AddColumn(_logger, "TypedBaseItems", "Path", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "StartDate", "DATETIME"); - _connection.AddColumn(_logger, "TypedBaseItems", "EndDate", "DATETIME"); - _connection.AddColumn(_logger, "TypedBaseItems", "ChannelId", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsMovie", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsSports", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsKids", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "CommunityRating", "Float"); - _connection.AddColumn(_logger, "TypedBaseItems", "CustomRating", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "IndexNumber", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsLocked", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "Name", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "OfficialRating", "Text"); - - _connection.AddColumn(_logger, "TypedBaseItems", "MediaType", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "Overview", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "ParentIndexNumber", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "PremiereDate", "DATETIME"); - _connection.AddColumn(_logger, "TypedBaseItems", "ProductionYear", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "ParentId", "GUID"); - _connection.AddColumn(_logger, "TypedBaseItems", "Genres", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "ParentalRatingValue", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "SchemaVersion", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "SortName", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "RunTimeTicks", "BIGINT"); - - _connection.AddColumn(_logger, "TypedBaseItems", "OfficialRatingDescription", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "HomePageUrl", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "VoteCount", "INT"); - _connection.AddColumn(_logger, "TypedBaseItems", "DisplayMediaType", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "DateCreated", "DATETIME"); - _connection.AddColumn(_logger, "TypedBaseItems", "DateModified", "DATETIME"); - - _connection.AddColumn(_logger, "TypedBaseItems", "ForcedSortName", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsOffline", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "LocationType", "Text"); - - _connection.AddColumn(_logger, "TypedBaseItems", "IsSeries", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsLive", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsNews", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsPremiere", "BIT"); - - _connection.AddColumn(_logger, "TypedBaseItems", "EpisodeTitle", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsRepeat", "BIT"); - - _connection.AddColumn(_logger, "TypedBaseItems", "PreferredMetadataLanguage", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "PreferredMetadataCountryCode", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "IsHD", "BIT"); - _connection.AddColumn(_logger, "TypedBaseItems", "ExternalEtag", "Text"); - _connection.AddColumn(_logger, "TypedBaseItems", "DateLastRefreshed", "DATETIME"); + _connection.RunQueries(queries, Logger); + + _connection.AddColumn(Logger, "AncestorIds", "AncestorIdText", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "Path", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "StartDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "EndDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "ChannelId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsMovie", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsSports", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsKids", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "CommunityRating", "Float"); + _connection.AddColumn(Logger, "TypedBaseItems", "CustomRating", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IndexNumber", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsLocked", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "Name", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRating", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "MediaType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Overview", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ParentIndexNumber", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "PremiereDate", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "ProductionYear", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "ParentId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "Genres", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ParentalRatingValue", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SchemaVersion", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "RunTimeTicks", "BIGINT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRatingDescription", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "HomePageUrl", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "VoteCount", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "DisplayMediaType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateCreated", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateModified", "DATETIME"); + + _connection.AddColumn(Logger, "TypedBaseItems", "ForcedSortName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsOffline", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "LocationType", "Text"); + + _connection.AddColumn(Logger, "TypedBaseItems", "IsSeries", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsLive", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsNews", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsPremiere", "BIT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "EpisodeTitle", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsRepeat", "BIT"); + + _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataLanguage", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataCountryCode", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsHD", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalEtag", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastRefreshed", "DATETIME"); + + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastSaved", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsInMixedFolder", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "LockedFields", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Studios", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Audio", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalServiceId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "Tags", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsFolder", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "InheritedParentalRatingValue", "INT"); + _connection.AddColumn(Logger, "TypedBaseItems", "UnratedType", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "TopParentId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsItemByName", "BIT"); PrepareStatements(); - new MediaStreamColumns(_connection, _logger).AddColumns(); + new MediaStreamColumns(_connection, Logger).AddColumns(); var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); if (File.Exists(chapterDbFile)) @@ -229,11 +254,11 @@ namespace MediaBrowser.Server.Implementations.Persistence "REPLACE INTO mediastreams("+columns+") SELECT "+columns+" FROM MediaInfoOld.mediastreams;" }; - _connection.RunQueries(queries, _logger); + _connection.RunQueries(queries, Logger); } catch (Exception ex) { - _logger.ErrorException("Error migrating media info database", ex); + Logger.ErrorException("Error migrating media info database", ex); } finally { @@ -253,11 +278,11 @@ namespace MediaBrowser.Server.Implementations.Persistence "REPLACE INTO "+ChaptersTableName+"(ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) SELECT ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath FROM ChaptersOld.Chapters;" }; - _connection.RunQueries(queries, _logger); + _connection.RunQueries(queries, Logger); } catch (Exception ex) { - _logger.ErrorException("Error migrating chapter database", ex); + Logger.ErrorException("Error migrating chapter database", ex); } finally { @@ -273,15 +298,10 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception ex) { - _logger.ErrorException("Error deleting file {0}", ex, file); + Logger.ErrorException("Error deleting file {0}", ex, file); } } - /// <summary> - /// The _write lock - /// </summary> - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); - private readonly string[] _retriveItemColumns = { "type", @@ -307,7 +327,27 @@ namespace MediaBrowser.Server.Implementations.Persistence "PreferredMetadataCountryCode", "IsHD", "ExternalEtag", - "DateLastRefreshed" + "DateLastRefreshed", + "Name", + "Path", + "PremiereDate", + "Overview", + "ParentIndexNumber", + "ProductionYear", + "OfficialRating", + "OfficialRatingDescription", + "HomePageUrl", + "DisplayMediaType", + "ForcedSortName", + "RunTimeTicks", + "VoteCount", + "DateCreated", + "DateModified", + "guid", + "Genres", + "ParentId", + "Audio", + "ExternalServiceId" }; private readonly string[] _mediaStreamSaveColumns = @@ -338,7 +378,8 @@ namespace MediaBrowser.Server.Implementations.Persistence "IsAnamorphic", "RefFrames", "IsCabac", - "KeyFrames" + "CodecTag", + "Comment" }; /// <summary> @@ -378,6 +419,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "ParentId", "Genres", "ParentalRatingValue", + "InheritedParentalRatingValue", "SchemaVersion", "SortName", "RunTimeTicks", @@ -394,7 +436,18 @@ namespace MediaBrowser.Server.Implementations.Persistence "PreferredMetadataCountryCode", "IsHD", "ExternalEtag", - "DateLastRefreshed" + "DateLastRefreshed", + "DateLastSaved", + "IsInMixedFolder", + "LockedFields", + "Studios", + "Audio", + "ExternalServiceId", + "Tags", + "IsFolder", + "UnratedType", + "TopParentId", + "IsItemByName" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -438,6 +491,17 @@ namespace MediaBrowser.Server.Implementations.Persistence _savePersonCommand.Parameters.Add(_savePersonCommand, "@SortOrder"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@ListOrder"); + // Ancestors + _deleteAncestorsCommand = _connection.CreateCommand(); + _deleteAncestorsCommand.CommandText = "delete from AncestorIds where ItemId=@Id"; + _deleteAncestorsCommand.Parameters.Add(_deleteAncestorsCommand, "@Id"); + + _saveAncestorCommand = _connection.CreateCommand(); + _saveAncestorCommand.CommandText = "insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values (@ItemId, @AncestorId, @AncestorIdText)"; + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@ItemId"); + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorId"); + _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorIdText"); + // Chapters _deleteChaptersCommand = _connection.CreateCommand(); _deleteChaptersCommand.CommandText = "delete from " + ChaptersTableName + " where ItemId=@ItemId"; @@ -467,6 +531,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { _saveStreamCommand.Parameters.Add(_saveStreamCommand, "@" + col); } + + _updateInheritedRatingCommand = _connection.CreateCommand(); + _updateInheritedRatingCommand.CommandText = "Update TypedBaseItems set InheritedParentalRatingValue=@InheritedParentalRatingValue where Guid=@Guid"; + _updateInheritedRatingCommand.Parameters.Add(_updateInheritedRatingCommand, "@InheritedParentalRatingValue"); + _updateInheritedRatingCommand.Parameters.Add(_updateInheritedRatingCommand, "@Guid"); } /// <summary> @@ -508,7 +577,7 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -592,7 +661,8 @@ namespace MediaBrowser.Server.Implementations.Persistence } _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Genres.ToArray()); - _saveItemCommand.GetParameter(index++).Value = item.GetParentalRatingValue(); + _saveItemCommand.GetParameter(index++).Value = item.GetParentalRatingValue() ?? 0; + _saveItemCommand.GetParameter(index++).Value = item.GetInheritedParentalRatingValue() ?? 0; _saveItemCommand.GetParameter(index++).Value = LatestSchemaVersion; _saveItemCommand.GetParameter(index++).Value = item.SortName; @@ -623,9 +693,62 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.DateLastRefreshed; } + _saveItemCommand.GetParameter(index++).Value = item.DateLastSaved; + _saveItemCommand.GetParameter(index++).Value = item.IsInMixedFolder; + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()); + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Studios.ToArray()); + + if (item.Audio.HasValue) + { + _saveItemCommand.GetParameter(index++).Value = item.Audio.Value.ToString(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + var tvItem = item as ILiveTvItem; + if (tvItem != null) + { + _saveItemCommand.GetParameter(index++).Value = tvItem.ServiceName; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Tags.ToArray()); + _saveItemCommand.GetParameter(index++).Value = item.IsFolder; + + _saveItemCommand.GetParameter(index++).Value = item.GetBlockUnratedType().ToString(); + + var topParent = item.GetTopParent(); + if (topParent != null) + { + _saveItemCommand.GetParameter(index++).Value = topParent.Id.ToString("N"); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + var isByName = false; + var byName = item as IItemByName; + if (byName != null) + { + var dualAccess = item as IHasDualAccess; + isByName = dualAccess == null || dualAccess.IsAccessedByName; + } + _saveItemCommand.GetParameter(index++).Value = isByName; + _saveItemCommand.Transaction = transaction; _saveItemCommand.ExecuteNonQuery(); + + if (item.SupportsAncestors) + { + UpdateAncestors(item.Id, item.GetAncestorIds().Distinct().ToList(), transaction); + } } transaction.Commit(); @@ -641,7 +764,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save items:", e); + Logger.ErrorException("Failed to save items:", e); if (transaction != null) { @@ -657,7 +780,7 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -701,27 +824,37 @@ namespace MediaBrowser.Server.Implementations.Persistence if (type == null) { - _logger.Debug("Unknown type {0}", typeString); + Logger.Debug("Unknown type {0}", typeString); return null; } - BaseItem item; + BaseItem item = null; using (var stream = reader.GetMemoryStream(1)) { try { item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; + } + catch (SerializationException ex) + { + Logger.ErrorException("Error deserializing item", ex); + } - if (item == null) + if (item == null) + { + try + { + item = Activator.CreateInstance(type) as BaseItem; + } + catch { - return null; } } - catch (SerializationException ex) + + if (item == null) { - _logger.ErrorException("Error deserializing item", ex); return null; } } @@ -844,6 +977,107 @@ namespace MediaBrowser.Server.Implementations.Persistence item.DateLastRefreshed = reader.GetDateTime(23).ToUniversalTime(); } + if (!reader.IsDBNull(24)) + { + item.Name = reader.GetString(24); + } + + if (!reader.IsDBNull(25)) + { + item.Path = reader.GetString(25); + } + + if (!reader.IsDBNull(26)) + { + item.PremiereDate = reader.GetDateTime(26).ToUniversalTime(); + } + + if (!reader.IsDBNull(27)) + { + item.Overview = reader.GetString(27); + } + + if (!reader.IsDBNull(28)) + { + item.ParentIndexNumber = reader.GetInt32(28); + } + + if (!reader.IsDBNull(29)) + { + item.ProductionYear = reader.GetInt32(29); + } + + if (!reader.IsDBNull(30)) + { + item.OfficialRating = reader.GetString(30); + } + + if (!reader.IsDBNull(31)) + { + item.OfficialRating = reader.GetString(31); + } + + if (!reader.IsDBNull(32)) + { + item.HomePageUrl = reader.GetString(32); + } + + if (!reader.IsDBNull(33)) + { + item.DisplayMediaType = reader.GetString(33); + } + + if (!reader.IsDBNull(34)) + { + item.ForcedSortName = reader.GetString(34); + } + + if (!reader.IsDBNull(35)) + { + item.RunTimeTicks = reader.GetInt64(35); + } + + if (!reader.IsDBNull(36)) + { + item.VoteCount = reader.GetInt32(36); + } + + if (!reader.IsDBNull(37)) + { + item.DateCreated = reader.GetDateTime(37).ToUniversalTime(); + } + + if (!reader.IsDBNull(38)) + { + item.DateModified = reader.GetDateTime(38).ToUniversalTime(); + } + + item.Id = reader.GetGuid(39); + + if (!reader.IsDBNull(40)) + { + item.Genres = reader.GetString(40).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + + if (!reader.IsDBNull(41)) + { + item.ParentId = reader.GetGuid(41); + } + + if (!reader.IsDBNull(42)) + { + item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(42), true); + } + + if (!reader.IsDBNull(43)) + { + var tvItem = item as ILiveTvItem; + if (tvItem != null) + { + tvItem.ServiceName = reader.GetString(43); + } + } + return item; } @@ -1006,7 +1240,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -1053,7 +1287,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save chapters:", e); + Logger.ErrorException("Failed to save chapters:", e); if (transaction != null) { @@ -1069,62 +1303,21 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() + protected override void CloseConnection() { - Dispose(true); - GC.SuppressFinalize(this); - } - - private readonly object _disposeLock = new object(); - - private bool _disposed; - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); - } - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) + if (_connection != null) { - _disposed = true; - - try - { - lock (_disposeLock) - { - _writeLock.Wait(); - - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } - catch (Exception ex) + if (_connection.IsOpen()) { - _logger.ErrorException("Error disposing database", ex); + _connection.Close(); } + + _connection.Dispose(); + _connection = null; } } @@ -1168,6 +1361,8 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = parentId; + //Logger.Debug(cmd.CommandText); + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) @@ -1213,6 +1408,50 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems"; + + var whereClauses = GetWhereClauses(query, cmd, true); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + cmd.CommandText += whereText; + + cmd.CommandText += GetOrderByText(query); + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + } + + //Logger.Debug(cmd.CommandText); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + var item = GetItem(reader); + if (item != null) + { + yield return item; + } + } + } + } + } + public QueryResult<BaseItem> GetItems(InternalItemsQuery query) { if (query == null) @@ -1249,7 +1488,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; - _logger.Debug(cmd.CommandText); + Logger.Debug(cmd.CommandText); var list = new List<BaseItem>(); var count = 0; @@ -1293,6 +1532,12 @@ namespace MediaBrowser.Server.Implementations.Persistence private string MapOrderByField(string name) { + if (string.Equals(name, "airtime", StringComparison.OrdinalIgnoreCase)) + { + // TODO + return "SortName"; + } + return name; } @@ -1326,9 +1571,9 @@ namespace MediaBrowser.Server.Implementations.Persistence var list = new List<Guid>(); - _logger.Debug(cmd.CommandText); + Logger.Debug(cmd.CommandText); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { @@ -1379,7 +1624,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var list = new List<Tuple<Guid, string>>(); var count = 0; - _logger.Debug(cmd.CommandText); + Logger.Debug(cmd.CommandText); using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { @@ -1448,7 +1693,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var list = new List<Guid>(); var count = 0; - _logger.Debug(cmd.CommandText); + Logger.Debug(cmd.CommandText); using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { @@ -1512,6 +1757,11 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("IsSports=@IsSports"); cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; } + if (query.IsFolder.HasValue) + { + whereClauses.Add("IsFolder=@IsFolder"); + cmd.Parameters.Add(cmd, "@IsFolder", DbType.Boolean).Value = query.IsFolder; + } var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); if (includeTypes.Length == 1) @@ -1572,6 +1822,12 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.Parameters.Add(cmd, "@MinStartDate", DbType.Date).Value = query.MinStartDate.Value; } + if (query.MinPremiereDate.HasValue) + { + whereClauses.Add("PremiereDate>=@MinPremiereDate"); + cmd.Parameters.Add(cmd, "@MinPremiereDate", DbType.Date).Value = query.MinPremiereDate.Value; + } + if (query.MaxStartDate.HasValue) { whereClauses.Add("StartDate<=@MaxStartDate"); @@ -1623,7 +1879,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.MaxParentalRating.HasValue) { - whereClauses.Add("(ParentalRatingValue is NULL OR ParentalRatingValue<=@MaxParentalRating)"); + whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating"); cmd.Parameters.Add(cmd, "@MaxParentalRating", DbType.Int32).Value = query.MaxParentalRating.Value; } @@ -1631,11 +1887,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (query.HasParentalRating.Value) { - whereClauses.Add("ParentalRatingValue NOT NULL"); + whereClauses.Add("InheritedParentalRatingValue > 0"); } else { - whereClauses.Add("ParentalRatingValue IS NULL"); + whereClauses.Add("InheritedParentalRatingValue = 0"); } } @@ -1646,7 +1902,78 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); } } + if (query.ExcludeLocationTypes.Length == 1) + { + whereClauses.Add("LocationType<>@LocationType"); + cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.ExcludeLocationTypes[0].ToString(); + } + if (query.ExcludeLocationTypes.Length > 1) + { + var val = string.Join(",", query.ExcludeLocationTypes.Select(i => "'" + i + "'").ToArray()); + whereClauses.Add("LocationType not in (" + val + ")"); + } + + var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; + + if (query.TopParentIds.Length == 1) + { + if (enableItemsByName) + { + whereClauses.Add("(TopParentId=@TopParentId or IsItemByName=@IsItemByName)"); + cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + } + else + { + whereClauses.Add("(TopParentId=@TopParentId)"); + } + cmd.Parameters.Add(cmd, "@TopParentId", DbType.String).Value = query.TopParentIds[0]; + } + if (query.TopParentIds.Length > 1) + { + var val = string.Join(",", query.TopParentIds.Select(i => "'" + i + "'").ToArray()); + + if (enableItemsByName) + { + whereClauses.Add("(IsItemByName=@IsItemByName or TopParentId in (" + val + "))"); + cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + } + else + { + whereClauses.Add("(TopParentId in (" + val + "))"); + } + } + + if (query.AncestorIds.Length == 1) + { + whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); + cmd.Parameters.Add(cmd, "@AncestorId", DbType.Guid).Value = new Guid(query.AncestorIds[0]); + } + if (query.AncestorIds.Length > 1) + { + var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + new Guid(i).ToString("N") + "'").ToArray()); + whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + } + + if (query.BlockUnratedItems.Length == 1) + { + whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)"); + cmd.Parameters.Add(cmd, "@UnratedType", DbType.String).Value = query.BlockUnratedItems[0].ToString(); + } + if (query.BlockUnratedItems.Length > 1) + { + var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'").ToArray()); + whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause)); + } + + //var excludeTagIndex = 0; + //foreach (var excludeTag in query.ExcludeTags) + //{ + // whereClauses.Add("Tags not like @excludeTag" + excludeTagIndex); + // cmd.Parameters.Add(cmd, "@excludeTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + // excludeTagIndex++; + //} + if (addPaging) { if (query.StartIndex.HasValue && query.StartIndex.Value > 0) @@ -1703,6 +2030,83 @@ namespace MediaBrowser.Server.Implementations.Persistence typeof(AggregateFolder) }; + public async Task UpdateInheritedValues(CancellationToken cancellationToken) + { + var newValues = new List<Tuple<Guid, int>>(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select Guid,InheritedParentalRatingValue,(select Max(ParentalRatingValue, (select COALESCE(MAX(ParentalRatingValue),0) from TypedBaseItems where guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid)))) as NewInheritedParentalRatingValue from typedbaseitems as Outer where InheritedParentalRatingValue <> NewInheritedParentalRatingValue"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + var newValue = reader.GetInt32(2); + + newValues.Add(new Tuple<Guid, int>(id, newValue)); + } + } + } + + if (newValues.Count == 0) + { + return; + } + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + foreach (var item in newValues) + { + _updateInheritedRatingCommand.GetParameter(0).Value = item.Item1; + _updateInheritedRatingCommand.GetParameter(1).Value = item.Item2; + + _updateInheritedRatingCommand.Transaction = transaction; + _updateInheritedRatingCommand.ExecuteNonQuery(); + + _updateInheritedRatingCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Error running query:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + private static Dictionary<string, string[]> GetTypeMapDictionary() { var dict = new Dictionary<string, string[]>(); @@ -1712,6 +2116,8 @@ namespace MediaBrowser.Server.Implementations.Persistence dict[t.Name] = new[] { t.FullName }; } + dict["ChannelItem"] = new[] { typeof(ChannelVideoItem).FullName, typeof(ChannelAudioItem).FullName, typeof(ChannelFolderItem).FullName }; + dict["LiveTvItem"] = new[] { typeof(LiveTvAudioRecording).FullName, typeof(LiveTvVideoRecording).FullName, typeof(LiveTvChannel).FullName, typeof(LiveTvProgram).FullName }; dict["Recording"] = new[] { typeof(LiveTvAudioRecording).FullName, typeof(LiveTvVideoRecording).FullName }; dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; @@ -1742,7 +2148,7 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -1770,11 +2176,16 @@ namespace MediaBrowser.Server.Implementations.Persistence _deleteStreamsCommand.Transaction = transaction; _deleteStreamsCommand.ExecuteNonQuery(); + // Delete ancestors + _deleteAncestorsCommand.GetParameter(0).Value = id; + _deleteAncestorsCommand.Transaction = transaction; + _deleteAncestorsCommand.ExecuteNonQuery(); + // Delete the item _deleteItemCommand.GetParameter(0).Value = id; _deleteItemCommand.Transaction = transaction; _deleteItemCommand.ExecuteNonQuery(); - + transaction.Commit(); } catch (OperationCanceledException) @@ -1788,7 +2199,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save children:", e); + Logger.ErrorException("Failed to save children:", e); if (transaction != null) { @@ -1804,7 +2215,7 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -1822,7 +2233,7 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -1861,7 +2272,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save children:", e); + Logger.ErrorException("Failed to save children:", e); if (transaction != null) { @@ -1877,7 +2288,7 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -2003,6 +2414,38 @@ namespace MediaBrowser.Server.Implementations.Persistence return whereClauses; } + private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (ancestorIds == null) + { + throw new ArgumentNullException("ancestorIds"); + } + + CheckDisposed(); + + // First delete + _deleteAncestorsCommand.GetParameter(0).Value = itemId; + _deleteAncestorsCommand.Transaction = transaction; + + _deleteAncestorsCommand.ExecuteNonQuery(); + + foreach (var ancestorId in ancestorIds) + { + _saveAncestorCommand.GetParameter(0).Value = itemId; + _saveAncestorCommand.GetParameter(1).Value = ancestorId; + _saveAncestorCommand.GetParameter(2).Value = ancestorId.ToString("N"); + + _saveAncestorCommand.Transaction = transaction; + + _saveAncestorCommand.ExecuteNonQuery(); + } + } + public async Task UpdatePeople(Guid itemId, List<PersonInfo> people) { if (itemId == Guid.Empty) @@ -2019,7 +2462,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var cancellationToken = CancellationToken.None; - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -2065,7 +2508,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save people:", e); + Logger.ErrorException("Failed to save people:", e); if (transaction != null) { @@ -2081,7 +2524,7 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -2168,7 +2611,7 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -2220,7 +2663,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; _saveStreamCommand.GetParameter(index++).Value = stream.IsCabac; - _saveStreamCommand.GetParameter(index++).Value = null; + _saveStreamCommand.GetParameter(index++).Value = stream.CodecTag; + _saveStreamCommand.GetParameter(index++).Value = stream.Comment; _saveStreamCommand.Transaction = transaction; _saveStreamCommand.ExecuteNonQuery(); @@ -2239,7 +2683,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save media streams:", e); + Logger.ErrorException("Failed to save media streams:", e); if (transaction != null) { @@ -2255,7 +2699,7 @@ namespace MediaBrowser.Server.Implementations.Persistence transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -2376,10 +2820,12 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(26)) { - var frames = reader.GetString(26); - if (!string.IsNullOrWhiteSpace(frames)) - { - } + item.CodecTag = reader.GetString(26); + } + + if (!reader.IsDBNull(27)) + { + item.Comment = reader.GetString(27); } return item; diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index 4d3e091b1..fbf514423 100644 --- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -34,11 +34,6 @@ namespace MediaBrowser.Server.Implementations.Playlists } } - public override bool IsHiddenFromUser(User user) - { - return false; - } - public override string CollectionType { get { return Model.Entities.CollectionType.Playlists; } diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs index d9e9fac2a..4413a7ddf 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Server.Implementations.Playlists return subItem; } - var parent = subItem.Parent; + var parent = subItem.GetParent(); if (parent != null && parent.HasImage(ImageType.Primary)) { diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs index d9b3ed755..048e2bf8d 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -264,6 +264,7 @@ namespace MediaBrowser.Server.Implementations.Playlists public Folder GetPlaylistsFolder(string userId) { return _libraryManager.RootFolder.Children.OfType<PlaylistsFolder>() + .FirstOrDefault() ?? _libraryManager.GetUserRootFolder().Children.OfType<PlaylistsFolder>() .FirstOrDefault(); } } diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs index 2f219c299..64ae249cd 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/RefreshMediaLibraryTask.cs @@ -43,11 +43,6 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks }.ToList(); - if (!_config.Configuration.DisableStartupScan) - { - list.Add(new StartupTrigger()); - } - return list; } diff --git a/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs b/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs index b36db51b3..b932f0cac 100644 --- a/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs +++ b/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs @@ -13,19 +13,17 @@ using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Security { - public class AuthenticationRepository : IAuthenticationRepository + public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository { private IDbConnection _connection; - private readonly ILogger _logger; - private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); private readonly IServerApplicationPaths _appPaths; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private IDbCommand _saveInfoCommand; - public AuthenticationRepository(ILogger logger, IServerApplicationPaths appPaths) + public AuthenticationRepository(ILogManager logManager, IServerApplicationPaths appPaths) + : base(logManager) { - _logger = logger; _appPaths = appPaths; } @@ -33,7 +31,7 @@ namespace MediaBrowser.Server.Implementations.Security { var dbFile = Path.Combine(_appPaths.DataPath, "authentication.db"); - _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); string[] queries = { @@ -46,9 +44,9 @@ namespace MediaBrowser.Server.Implementations.Security "pragma shrink_memory" }; - _connection.RunQueries(queries, _logger); + _connection.RunQueries(queries, Logger); - _connection.AddColumn(_logger, "AccessTokens", "AppVersion", "TEXT"); + _connection.AddColumn(Logger, "AccessTokens", "AppVersion", "TEXT"); PrepareStatements(); } @@ -86,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.Security cancellationToken.ThrowIfCancellationRequested(); - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; @@ -124,7 +122,7 @@ namespace MediaBrowser.Server.Implementations.Security } catch (Exception e) { - _logger.ErrorException("Failed to save record:", e); + Logger.ErrorException("Failed to save record:", e); if (transaction != null) { @@ -140,7 +138,7 @@ namespace MediaBrowser.Server.Implementations.Security transaction.Dispose(); } - _writeLock.Release(); + WriteLock.Release(); } } @@ -305,7 +303,7 @@ namespace MediaBrowser.Server.Implementations.Security { info.DeviceName = reader.GetString(5); } - + if (!reader.IsDBNull(6)) { info.UserId = reader.GetString(6); @@ -318,49 +316,21 @@ namespace MediaBrowser.Server.Implementations.Security { info.DateRevoked = reader.GetDateTime(9).ToUniversalTime(); } - - return info; - } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + return info; } - private readonly object _disposeLock = new object(); - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) + protected override void CloseConnection() { - if (dispose) + if (_connection != null) { - try + if (_connection.IsOpen()) { - lock (_disposeLock) - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } - catch (Exception ex) - { - _logger.ErrorException("Error disposing database", ex); + _connection.Close(); } + + _connection.Dispose(); + _connection = null; } } } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 8d3273421..a9ce5ba54 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -1621,17 +1621,17 @@ namespace MediaBrowser.Server.Implementations.Session if (backropItem == null) { - backropItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Backdrop)); + backropItem = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Backdrop)); } if (thumbItem == null) { - thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb)); + thumbItem = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Thumb)); } if (logoItem == null) { - logoItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Logo)); + logoItem = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Logo)); } if (thumbItem != null) diff --git a/MediaBrowser.Server.Implementations/Sorting/IsFolderComparer.cs b/MediaBrowser.Server.Implementations/Sorting/IsFolderComparer.cs index 9edbd90dc..d2341d065 100644 --- a/MediaBrowser.Server.Implementations/Sorting/IsFolderComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/IsFolderComparer.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <value>The name.</value> public string Name { - get { return ItemSortBy.Album; } + get { return ItemSortBy.IsFolder; } } } } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index db6a753b7..50a960956 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -341,7 +341,7 @@ namespace MediaBrowser.Server.Implementations.Sync if (primaryImage == null) { - var parentWithImage = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary)); + var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); if (parentWithImage != null) { @@ -380,7 +380,7 @@ namespace MediaBrowser.Server.Implementations.Sync if (primaryImage == null) { - var parentWithImage = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary)); + var parentWithImage = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); if (parentWithImage != null) { @@ -491,6 +491,11 @@ namespace MediaBrowser.Server.Implementations.Sync public bool SupportsSync(BaseItem item) { + if (item == null) + { + throw new ArgumentNullException("item"); + } + if (item is Playlist) { return true; @@ -739,10 +744,10 @@ namespace MediaBrowser.Server.Implementations.Sync var requiresSaving = false; var removeFromDevice = false; - var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); - if (request.LocalItemIds.Contains(jobItem.ItemId, StringComparer.OrdinalIgnoreCase)) { + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + var job = _repo.GetJob(jobItem.JobId); var user = _userManager.GetUserById(job.UserId); @@ -845,10 +850,10 @@ namespace MediaBrowser.Server.Implementations.Sync var requiresSaving = false; var removeFromDevice = false; - var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); - if (request.SyncJobItemIds.Contains(jobItem.Id, StringComparer.OrdinalIgnoreCase)) { + var libraryItem = _libraryManager.GetItemById(jobItem.ItemId); + var job = _repo.GetJob(jobItem.JobId); var user = _userManager.GetUserById(job.UserId); diff --git a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs index d266a534d..7a1f276f9 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.Sync _connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); _connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); - + PrepareStatements(); } @@ -800,24 +800,6 @@ namespace MediaBrowser.Server.Implementations.Sync return item; } - private bool _disposed; - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); - } - } - - protected override void Dispose(bool dispose) - { - if (dispose) - { - _disposed = true; - } - base.Dispose(dispose); - } - protected override void CloseConnection() { if (_connection != null) diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs index efd37fa00..456a1fcc1 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Server.Implementations.Sync { diff --git a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs b/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs index f34b43e43..69d1c89fe 100644 --- a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs +++ b/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs @@ -36,8 +36,12 @@ namespace MediaBrowser.Server.Implementations.TV ? new string[] { } : new[] { request.ParentId }; - var items = GetAllLibraryItems(user, parentIds, i => i is Series) - .Cast<Series>(); + var items = _libraryManager.GetItems(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + SortOrder = SortOrder.Ascending + + }, parentIds).Cast<Series>(); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items); @@ -54,9 +58,12 @@ namespace MediaBrowser.Server.Implementations.TV throw new ArgumentException("User not found"); } - var items = parentsFolders - .SelectMany(i => i.GetRecursiveChildren(user, s => s is Series)) - .Cast<Series>(); + var items = _libraryManager.GetItems(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Series).Name }, + SortOrder = SortOrder.Ascending + + }, parentsFolders.Select(i => i.Id.ToString("N"))).Cast<Series>(); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items); @@ -64,27 +71,6 @@ namespace MediaBrowser.Server.Implementations.TV return GetResult(episodes, null, request); } - private IEnumerable<BaseItem> GetAllLibraryItems(User user, string[] parentIds, Func<BaseItem,bool> filter) - { - if (parentIds.Length > 0) - { - return parentIds.SelectMany(i => - { - var folder = (Folder)_libraryManager.GetItemById(new Guid(i)); - - return folder.GetRecursiveChildren(user, filter); - - }); - } - - if (user == null) - { - throw new ArgumentException("User not found"); - } - - return user.RootFolder.GetRecursiveChildren(user, filter); - } - public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<Series> series) { // Avoid implicitly captured closure diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index c3c6e6e25..98c27786f 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -1,8 +1,9 @@ <?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="CommonIO" version="1.0.0.7" targetFramework="net45" />
+ <package id="Emby.XmlTv" version="1.0.0.46" targetFramework="net45" />
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
- <package id="MediaBrowser.Naming" version="1.0.0.46" targetFramework="net45" />
+ <package id="MediaBrowser.Naming" version="1.0.0.47" targetFramework="net45" />
<package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
<package id="morelinq" version="1.4.0" targetFramework="net45" />
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index 2d625f810..e61a64646 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -317,12 +317,20 @@ namespace MediaBrowser.Server.Startup.Common /// <returns>Task.</returns> public override async Task RunStartupTasks() { + if (ServerConfigurationManager.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion && + ServerConfigurationManager.Configuration.IsStartupWizardCompleted) + { + TaskManager.SuspendTriggers = true; + } + await base.RunStartupTasks().ConfigureAwait(false); Logger.Info("ServerId: {0}", SystemId); Logger.Info("Core startup complete"); HttpServer.GlobalResponse = null; + PerformPostInitMigrations(); + Parallel.ForEach(GetExports<IServerEntryPoint>(), entryPoint => { try @@ -336,8 +344,6 @@ namespace MediaBrowser.Server.Startup.Common }); LogManager.RemoveConsoleOutput(); - - PerformPostInitMigrations(); } public override Task Init(IProgress<double> progress) @@ -367,7 +373,8 @@ namespace MediaBrowser.Server.Startup.Common { var migrations = new List<IVersionMigration> { - new Release5767(ServerConfigurationManager, TaskManager) + new OmdbEpisodeProviderMigration(ServerConfigurationManager), + new DbMigration(ServerConfigurationManager, TaskManager) }; foreach (var task in migrations) @@ -423,7 +430,7 @@ namespace MediaBrowser.Server.Startup.Common UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager); RegisterSingleInstance(UserManager); - LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager); + LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); RegisterSingleInstance(LibraryManager); var musicManager = new MusicManager(LibraryManager); @@ -469,7 +476,7 @@ namespace MediaBrowser.Server.Startup.Common ConnectManager = new ConnectManager(LogManager.GetLogger("Connect"), ApplicationPaths, JsonSerializer, encryptionManager, HttpClient, this, ServerConfigurationManager, UserManager, ProviderManager, SecurityManager, FileSystemManager); RegisterSingleInstance(ConnectManager); - DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); + DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); RegisterSingleInstance(DeviceManager); var newsService = new Implementations.News.NewsService(ApplicationPaths, JsonSerializer); @@ -507,7 +514,7 @@ namespace MediaBrowser.Server.Startup.Common UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); RegisterSingleInstance(UserViewManager); - var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, LocalizationManager, ChannelManager, MediaSourceManager); + var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, LocalizationManager, ChannelManager, MediaSourceManager, UserViewManager); RegisterSingleInstance<IContentDirectory>(contentDirectory); var mediaRegistrar = new MediaReceiverRegistrar(LogManager.GetLogger("MediaReceiverRegistrar"), HttpClient, ServerConfigurationManager); @@ -646,11 +653,19 @@ namespace MediaBrowser.Server.Startup.Common /// <returns>Task{IUserRepository}.</returns> private async Task<IUserRepository> GetUserRepository() { - var repo = new SqliteUserRepository(LogManager, ApplicationPaths, JsonSerializer); + try + { + var repo = new SqliteUserRepository(LogManager, ApplicationPaths, JsonSerializer); - await repo.Initialize().ConfigureAwait(false); + await repo.Initialize().ConfigureAwait(false); - return repo; + return repo; + } + catch (Exception ex) + { + Logger.ErrorException("Error opening user db", ex); + throw; + } } /// <summary> @@ -668,7 +683,7 @@ namespace MediaBrowser.Server.Startup.Common private async Task<IAuthenticationRepository> GetAuthenticationRepository() { - var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths); + var repo = new AuthenticationRepository(LogManager, ServerConfigurationManager.ApplicationPaths); await repo.Initialize().ConfigureAwait(false); @@ -677,7 +692,7 @@ namespace MediaBrowser.Server.Startup.Common private async Task<IActivityRepository> GetActivityLogRepository() { - var repo = new ActivityRepository(LogManager.GetLogger("ActivityRepository"), ServerConfigurationManager.ApplicationPaths); + var repo = new ActivityRepository(LogManager, ServerConfigurationManager.ApplicationPaths); await repo.Initialize().ConfigureAwait(false); @@ -969,10 +984,6 @@ namespace MediaBrowser.Server.Startup.Common { get { - if (!ServerConfigurationManager.Configuration.EnableAutoUpdate) - { - return false; - } #if DEBUG return false; #endif diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs index b055c250b..3b83e65b2 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs +++ b/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloadInfo.cs @@ -54,7 +54,7 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg info.FFMpegFilename = "ffmpeg.exe"; info.FFProbeFilename = "ffprobe.exe"; - info.Version = "20151111"; + info.Version = "20160131"; info.ArchiveType = "7z"; switch (environment.SystemArchitecture) @@ -83,13 +83,13 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg case Architecture.X86_X64: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20151111-win64.7z", + "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160131-win64.7z", "http://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20151109-git-480bad7-win64-static.7z" }; case Architecture.X86: return new[] { - "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20151111-win32.7z", + "https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160131-win32.7z", "http://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-20151109-git-480bad7-win32-static.7z" }; } diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 6dd2066bb..a4028d5b0 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -72,7 +72,8 @@ <Compile Include="INativeApp.cs" /> <Compile Include="MbLinkShortcutHandler.cs" /> <Compile Include="Migrations\IVersionMigration.cs" /> - <Compile Include="Migrations\Release5767.cs" /> + <Compile Include="Migrations\DbMigration.cs" /> + <Compile Include="Migrations\OmdbEpisodeProviderMigration.cs" /> <Compile Include="Migrations\RenameXmlOptions.cs" /> <Compile Include="NativeEnvironment.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> diff --git a/MediaBrowser.Server.Startup.Common/Migrations/DbMigration.cs b/MediaBrowser.Server.Startup.Common/Migrations/DbMigration.cs new file mode 100644 index 000000000..3cd92bcde --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/Migrations/DbMigration.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Server.Implementations.Persistence; + +namespace MediaBrowser.Server.Startup.Common.Migrations +{ + public class DbMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly ITaskManager _taskManager; + + public DbMigration(IServerConfigurationManager config, ITaskManager taskManager) + { + _config = config; + _taskManager = taskManager; + } + + public void Run() + { + if (_config.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion && + _config.Configuration.IsStartupWizardCompleted) + { + _taskManager.SuspendTriggers = true; + CleanDatabaseScheduledTask.EnableUnavailableMessage = true; + + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + + _taskManager.Execute<CleanDatabaseScheduledTask>(); + }); + } + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/Migrations/OmdbEpisodeProviderMigration.cs b/MediaBrowser.Server.Startup.Common/Migrations/OmdbEpisodeProviderMigration.cs new file mode 100644 index 000000000..00f6a692b --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/Migrations/OmdbEpisodeProviderMigration.cs @@ -0,0 +1,47 @@ +using MediaBrowser.Controller.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Startup.Common.Migrations +{ + class OmdbEpisodeProviderMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private const string _providerName = "The Open Movie Database"; + + public OmdbEpisodeProviderMigration(IServerConfigurationManager config) + { + _config = config; + } + + public void Run() + { + var migrationKey = this.GetType().FullName; + var migrationKeyList = _config.Configuration.Migrations.ToList(); + + if (!migrationKeyList.Contains(migrationKey)) + { + foreach (var metaDataOption in _config.Configuration.MetadataOptions) + { + if (metaDataOption.ItemType == "Episode") + { + var disabledFetchers = metaDataOption.DisabledMetadataFetchers.ToList(); + if (!disabledFetchers.Contains(_providerName)) + { + disabledFetchers.Add(_providerName); + metaDataOption.DisabledMetadataFetchers = disabledFetchers.ToArray(); + } + } + } + + migrationKeyList.Add(migrationKey); + _config.Configuration.Migrations = migrationKeyList.ToArray(); + _config.SaveConfiguration(); + } + + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs b/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs deleted file mode 100644 index 168230b87..000000000 --- a/MediaBrowser.Server.Startup.Common/Migrations/Release5767.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Server.Implementations.LiveTv; -using MediaBrowser.Server.Implementations.Persistence; -using MediaBrowser.Server.Implementations.ScheduledTasks; - -namespace MediaBrowser.Server.Startup.Common.Migrations -{ - public class Release5767 : IVersionMigration - { - private readonly IServerConfigurationManager _config; - private readonly ITaskManager _taskManager; - - public Release5767(IServerConfigurationManager config, ITaskManager taskManager) - { - _config = config; - _taskManager = taskManager; - } - - public async void Run() - { - var name = "5767.1"; - - if (_config.Configuration.Migrations.Contains(name, StringComparer.OrdinalIgnoreCase)) - { - return; - } - - Task.Run(async () => - { - await Task.Delay(3000).ConfigureAwait(false); - - _taskManager.QueueScheduledTask<CleanDatabaseScheduledTask>(); - }); - - // Wait a few minutes before marking this as done. Make sure the server doesn't get restarted. - await Task.Delay(300000).ConfigureAwait(false); - - var list = _config.Configuration.Migrations.ToList(); - list.Add(name); - _config.Configuration.Migrations = list.ToArray(); - _config.SaveConfiguration(); - } - } -} diff --git a/MediaBrowser.ServerApplication/BackgroundService.cs b/MediaBrowser.ServerApplication/BackgroundService.cs index da7537cd7..c091c08ee 100644 --- a/MediaBrowser.ServerApplication/BackgroundService.cs +++ b/MediaBrowser.ServerApplication/BackgroundService.cs @@ -14,18 +14,6 @@ namespace MediaBrowser.ServerApplication public static string GetExistingServiceName() { - try - { - if (ServiceController.GetServices().Any(s => s.ServiceName == "MediaBrowser")) - { - return "MediaBrowser"; - } - } - catch - { - return "MediaBrowser"; - } - return Name; } diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 12cd5c385..227026154 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -356,6 +356,11 @@ namespace MediaBrowser.WebDashboard.Api DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st"); DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src"); + _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked"), true); + _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked-element"), true); + _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "prism"), true); + _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "prism-element"), true); + if (string.Equals(mode, "cordova", StringComparison.OrdinalIgnoreCase)) { // Delete things that are unneeded in an attempt to keep the output as trim as possible diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index 4a6289771..67eb3d53b 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -477,7 +477,6 @@ namespace MediaBrowser.WebDashboard.Api var files = new[] { - "thirdparty/jquerymobile-1.4.5/jquery.mobile.custom.theme.css", "css/site.css", "css/librarymenu.css", "css/librarybrowser.css", diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index 165792291..95a17808d 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -98,15 +98,45 @@ </None>
</ItemGroup>
<ItemGroup>
+ <Content Include="dashboard-ui\autoorganizesmart.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\components\chromecasthelpers.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\autoorganizesmart.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\bower_components\fastclick\lib\fastclick.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\filterdialog\filterdialog.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\filterdialog\filterdialog.template.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\guestinviter\guestinviter.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\guestinviter\guestinviter.template.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\metadataeditor\metadataeditor.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\metadataeditor\personeditor.template.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\components\recordingcreator\recordingcreator.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\recordingcreator\recordingcreator.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\components\remotecontrol.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\components\remotecontrolautoplay.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -119,6 +149,12 @@ <Content Include="dashboard-ui\components\directorybrowser\directorybrowser.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\components\fileorganizer\fileorganizer.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="dashboard-ui\components\fileorganizer\fileorganizer.template.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\components\humanedate.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -152,7 +188,7 @@ <Content Include="dashboard-ui\components\medialibraryeditor\medialibraryeditor.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\components\metadataeditor\metadataeditor.js">
+ <Content Include="dashboard-ui\components\metadataeditor\personeditor.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\metadataeditor\metadataeditor.template.html">
@@ -161,9 +197,6 @@ <Content Include="dashboard-ui\components\playlisteditor\playlisteditor.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\components\prompt.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\components\tvguide\tvguide.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -281,6 +314,9 @@ <Content Include="dashboard-ui\scripts\mysyncsettings.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="dashboard-ui\scripts\autoorganizesmart.js">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
<Content Include="dashboard-ui\scripts\searchmenu.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -368,15 +404,9 @@ <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.controlgroup.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.controlgroup.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.js">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.panel.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -1039,309 +1069,6 @@ <Content Include="dashboard-ui\scripts\wizardsettings.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\ajax-loader.gif">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\action-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\action-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\alert-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\alert-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-l-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-l-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-r-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-r-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-d-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-l-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-l-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-r-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-r-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-l-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-l-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-r-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-r-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\arrow-u-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\audio-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\audio-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\back-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\back-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\bars-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\bars-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\bullets-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\bullets-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\calendar-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\calendar-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\camera-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\camera-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-d-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-d-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-l-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-l-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-r-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-r-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-u-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\carat-u-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\check-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\check-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\clock-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\clock-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\cloud-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\cloud-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\comment-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\comment-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\delete-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\delete-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\edit-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\edit-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\eye-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\eye-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\forbidden-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\forbidden-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\forward-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\forward-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\gear-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\gear-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\grid-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\grid-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\heart-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\heart-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\home-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\home-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\info-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\info-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\location-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\location-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\lock-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\lock-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\mail-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\mail-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\minus-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\minus-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\navigation-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\navigation-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\phone-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\phone-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\plus-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\plus-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\power-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\power-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\recycle-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\recycle-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\refresh-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\refresh-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\search-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\search-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\shop-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\shop-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\star-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\star-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\tag-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\tag-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\user-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\user-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\video-black.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
- <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-png\video-white.png">
- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
- </Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\images\icons-svg\action-black.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@@ -2473,6 +2200,9 @@ <None Include="dashboard-ui\strings\html\zh-HK.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
+ <None Include="dashboard-ui\strings\javascript\id.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
<None Include="dashboard-ui\strings\javascript\ko.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 40b974ee7..30243ff57 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -1173,6 +1173,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers { video.Video3DFormat = Video3DFormat.FullSideBySide; } + else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } } break; } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index c9fad8a7c..f1d4366b7 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -48,7 +48,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat; - writer.WriteElementString("disbanded", artist.EndDate.Value.ToString(formatString)); + writer.WriteElementString("disbanded", artist.EndDate.Value.ToLocalTime().ToString(formatString)); } var albums = artist diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index c114f9dd9..53a7926ca 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -404,6 +404,9 @@ namespace MediaBrowser.XbmcMetadata.Savers case Video3DFormat.HalfTopAndBottom: writer.WriteElementString("format3d", "HTAB"); break; + case Video3DFormat.MVC: + writer.WriteElementString("format3d", "MVC"); + break; } } } @@ -628,12 +631,12 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item is MusicArtist) { - writer.WriteElementString("formed", item.PremiereDate.Value.ToString(formatString)); + writer.WriteElementString("formed", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } else { - writer.WriteElementString("premiered", item.PremiereDate.Value.ToString(formatString)); - writer.WriteElementString("releasedate", item.PremiereDate.Value.ToString(formatString)); + writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } } @@ -643,7 +646,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = options.ReleaseDateFormat; - writer.WriteElementString("enddate", item.EndDate.Value.ToString(formatString)); + writer.WriteElementString("enddate", item.EndDate.Value.ToLocalTime().ToString(formatString)); } } diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index b08521794..5363efcdf 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat; - writer.WriteElementString("aired", episode.PremiereDate.Value.ToString(formatString)); + writer.WriteElementString("aired", episode.PremiereDate.Value.ToLocalTime().ToString(formatString)); } if (episode.AirsAfterSeasonNumber.HasValue) diff --git a/MediaBrowser.sln b/MediaBrowser.sln index d0fbd743e..a9b5020ec 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Performance2.psess = Performance2.psess Performance3.psess = Performance3.psess Performance4.psess = Performance4.psess + Performance5.psess = Performance5.psess EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget (2)", ".nuget (2)", "{E60FB157-87E2-4A41-8B04-27EA49B63B4D}" @@ -520,4 +521,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/Nuget/MediaBrowser.Common.Internal.nuspec b/Nuget/MediaBrowser.Common.Internal.nuspec index 8fa5b665d..c4f2bd130 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.638</version> + <version>3.0.641</version> <title>MediaBrowser.Common.Internal</title> <authors>Luke</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,9 +12,9 @@ <description>Contains common components shared by Emby Theater and Emby Server. Not intended for plugin developer consumption.</description> <copyright>Copyright © Emby 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.638" /> - <dependency id="NLog" version="3.2.1" /> - <dependency id="SimpleInjector" version="3.0.5" /> + <dependency id="MediaBrowser.Common" version="3.0.641" /> + <dependency id="NLog" version="4.2.3" /> + <dependency id="SimpleInjector" version="3.1.2" /> </dependencies> </metadata> <files> diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index cec3f63d5..984e03342 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.638</version> + <version>3.0.641</version> <title>MediaBrowser.Common</title> <authors>Emby Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Model.Signed.nuspec b/Nuget/MediaBrowser.Model.Signed.nuspec index 6f778882a..2b0826670 100644 --- a/Nuget/MediaBrowser.Model.Signed.nuspec +++ b/Nuget/MediaBrowser.Model.Signed.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"> <metadata> <id>MediaBrowser.Model.Signed</id> - <version>3.0.638</version> + <version>3.0.641</version> <title>MediaBrowser.Model - Signed Edition</title> <authors>Emby Team</authors> <owners>ebr,Luke,scottisafool</owners> diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index b6edd8719..80d3bc103 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.638</version> + <version>3.0.641</version> <title>Media Browser.Server.Core</title> <authors>Emby Team</authors> <owners>ebr,Luke,scottisafool</owners> @@ -12,7 +12,7 @@ <description>Contains core components required to build plugins for Emby Server.</description> <copyright>Copyright © Emby 2013</copyright> <dependencies> - <dependency id="MediaBrowser.Common" version="3.0.638" /> + <dependency id="MediaBrowser.Common" version="3.0.641" /> <dependency id="Interfaces.IO" version="1.0.0.5" /> </dependencies> </metadata> |
