diff options
Diffstat (limited to 'MediaBrowser.Api')
58 files changed, 1503 insertions, 2070 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 8233717ab..dc811812a 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -237,9 +237,12 @@ namespace MediaBrowser.Api { lock (_activeTranscodingJobs) { - var job = _activeTranscodingJobs.First(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); + var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); - _activeTranscodingJobs.Remove(job); + if (job != null) + { + _activeTranscodingJobs.Remove(job); + } } if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) @@ -349,7 +352,7 @@ namespace MediaBrowser.Api if (job.Type != TranscodingJobType.Progressive) { - timerDuration = 1800000; + timerDuration = 60000; } job.PingTimeout = timerDuration; @@ -488,13 +491,17 @@ namespace MediaBrowser.Api { try { - Logger.Info("Killing ffmpeg process for {0}", job.Path); + Logger.Info("Stopping ffmpeg process with q command for {0}", job.Path); //process.Kill(); process.StandardInput.WriteLine("q"); // Need to wait because killing is asynchronous - process.WaitForExit(5000); + if (!process.WaitForExit(5000)) + { + Logger.Info("Killing ffmpeg process for {0}", job.Path); + process.Kill(); + } } catch (Exception ex) { diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 30750b374..44a367be0 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Api } } } - + /// <summary> /// To the optimized serialized result using cache. /// </summary> @@ -115,12 +115,9 @@ namespace MediaBrowser.Api /// <returns>System.Object.</returns> protected object ToStaticFileResult(string path) { - return ResultFactory.GetStaticFileResult(Request, path); + return ResultFactory.GetStaticFileResult(Request, path).Result; } - private readonly char[] _dashReplaceChars = { '?', '/', '&' }; - private const char SlugChar = '-'; - protected DtoOptions GetDtoOptions(object request) { var options = new DtoOptions(); @@ -154,152 +151,122 @@ namespace MediaBrowser.Api protected MusicArtist GetArtist(string name, ILibraryManager libraryManager) { - return libraryManager.GetArtist(DeSlugArtistName(name, libraryManager)); - } - - protected Studio GetStudio(string name, ILibraryManager libraryManager) - { - return libraryManager.GetStudio(DeSlugStudioName(name, libraryManager)); - } - - protected Genre GetGenre(string name, ILibraryManager libraryManager) - { - return libraryManager.GetGenre(DeSlugGenreName(name, libraryManager)); - } + if (name.IndexOf(BaseItem.SlugChar) != -1) + { + var result = libraryManager.GetItemList(new InternalItemsQuery + { + SlugName = name, + IncludeItemTypes = new[] { typeof(MusicArtist).Name } - protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager) - { - return libraryManager.GetMusicGenre(DeSlugGenreName(name, libraryManager)); - } + }).OfType<MusicArtist>().FirstOrDefault(); - protected GameGenre GetGameGenre(string name, ILibraryManager libraryManager) - { - return libraryManager.GetGameGenre(DeSlugGameGenreName(name, libraryManager)); - } + if (result != null) + { + return result; + } + } - protected Person GetPerson(string name, ILibraryManager libraryManager) - { - return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager)); + return libraryManager.GetArtist(name); } - /// <summary> - /// Deslugs an artist name by finding the correct entry in the library - /// </summary> - /// <param name="name"></param> - /// <param name="libraryManager"></param> - /// <returns></returns> - protected string DeSlugArtistName(string name, ILibraryManager libraryManager) + protected Studio GetStudio(string name, ILibraryManager libraryManager) { - if (name.IndexOf(SlugChar) == -1) - { - return name; - } - - var items = libraryManager.GetItemList(new InternalItemsQuery + if (name.IndexOf(BaseItem.SlugChar) != -1) { - IncludeItemTypes = new[] { typeof(Audio).Name, typeof(MusicVideo).Name, typeof(MusicAlbum).Name } - }); - - return items - .OfType<IHasArtist>() - .SelectMany(i => i.AllArtists) - .DistinctNames() - .FirstOrDefault(i => + var result = libraryManager.GetItemList(new InternalItemsQuery { - i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); + SlugName = name, + IncludeItemTypes = new[] { typeof(Studio).Name } - return string.Equals(i, name, StringComparison.OrdinalIgnoreCase); + }).OfType<Studio>().FirstOrDefault(); + + if (result != null) + { + return result; + } + } - }) ?? name; + return libraryManager.GetStudio(name); } - /// <summary> - /// Deslugs a genre name by finding the correct entry in the library - /// </summary> - protected string DeSlugGenreName(string name, ILibraryManager libraryManager) + protected Genre GetGenre(string name, ILibraryManager libraryManager) { - if (name.IndexOf(SlugChar) == -1) + if (name.IndexOf(BaseItem.SlugChar) != -1) { - return name; - } - - return libraryManager.RootFolder.GetRecursiveChildren() - .SelectMany(i => i.Genres) - .DistinctNames() - .FirstOrDefault(i => + var result = libraryManager.GetItemList(new InternalItemsQuery { - i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); + SlugName = name, + IncludeItemTypes = new[] { typeof(Genre).Name } - return string.Equals(i, name, StringComparison.OrdinalIgnoreCase); + }).OfType<Genre>().FirstOrDefault(); + + if (result != null) + { + return result; + } + } - }) ?? name; + return libraryManager.GetGenre(name); } - protected string DeSlugGameGenreName(string name, ILibraryManager libraryManager) + protected MusicGenre GetMusicGenre(string name, ILibraryManager libraryManager) { - if (name.IndexOf(SlugChar) == -1) + if (name.IndexOf(BaseItem.SlugChar) != -1) { - return name; - } + var result = libraryManager.GetItemList(new InternalItemsQuery + { + SlugName = name, + IncludeItemTypes = new[] { typeof(MusicGenre).Name } - var items = libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Game).Name } - }); + }).OfType<MusicGenre>().FirstOrDefault(); - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .FirstOrDefault(i => + if (result != null) { - i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); - - return string.Equals(i, name, StringComparison.OrdinalIgnoreCase); + return result; + } + } - }) ?? name; + return libraryManager.GetMusicGenre(name); } - /// <summary> - /// Deslugs a studio name by finding the correct entry in the library - /// </summary> - protected string DeSlugStudioName(string name, ILibraryManager libraryManager) + protected GameGenre GetGameGenre(string name, ILibraryManager libraryManager) { - if (name.IndexOf(SlugChar) == -1) + if (name.IndexOf(BaseItem.SlugChar) != -1) { - return name; - } - - return libraryManager.RootFolder - .GetRecursiveChildren() - .SelectMany(i => i.Studios) - .DistinctNames() - .FirstOrDefault(i => + var result = libraryManager.GetItemList(new InternalItemsQuery { - i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); + SlugName = name, + IncludeItemTypes = new[] { typeof(GameGenre).Name } - return string.Equals(i, name, StringComparison.OrdinalIgnoreCase); + }).OfType<GameGenre>().FirstOrDefault(); - }) ?? name; + if (result != null) + { + return result; + } + } + + return libraryManager.GetGameGenre(name); } - /// <summary> - /// Deslugs a person name by finding the correct entry in the library - /// </summary> - protected string DeSlugPersonName(string name, ILibraryManager libraryManager) + protected Person GetPerson(string name, ILibraryManager libraryManager) { - if (name.IndexOf(SlugChar) == -1) + if (name.IndexOf(BaseItem.SlugChar) != -1) { - return name; - } - - return libraryManager.GetPeopleNames(new InternalPeopleQuery()) - .FirstOrDefault(i => + var result = libraryManager.GetItemList(new InternalItemsQuery { - i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar)); + SlugName = name, + IncludeItemTypes = new[] { typeof(Person).Name } - return string.Equals(i, name, StringComparison.OrdinalIgnoreCase); + }).OfType<Person>().FirstOrDefault(); + + if (result != null) + { + return result; + } + } - }) ?? name; + return libraryManager.GetPerson(name); } protected string GetPathValue(int index) diff --git a/MediaBrowser.Api/BrandingService.cs b/MediaBrowser.Api/BrandingService.cs index c900e4d06..e991565ad 100644 --- a/MediaBrowser.Api/BrandingService.cs +++ b/MediaBrowser.Api/BrandingService.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Api } [Route("/Branding/Css", "GET", Summary = "Gets custom css")] + [Route("/Branding/Css.css", "GET", Summary = "Gets custom css")] public class GetBrandingCss { } diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs index 446415fbb..2e5c252d6 100644 --- a/MediaBrowser.Api/ConfigurationService.cs +++ b/MediaBrowser.Api/ConfigurationService.cs @@ -9,7 +9,9 @@ using ServiceStack.Web; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.MediaEncoding; namespace MediaBrowser.Api { @@ -71,6 +73,16 @@ namespace MediaBrowser.Api } + [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")] + [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + public class UpdateMediaEncoderPath : IReturnVoid + { + [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Path { get; set; } + [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string PathType { get; set; } + } + public class ConfigurationService : BaseApiService { /// <summary> @@ -86,14 +98,22 @@ namespace MediaBrowser.Api private readonly IFileSystem _fileSystem; private readonly IProviderManager _providerManager; private readonly ILibraryManager _libraryManager; + private readonly IMediaEncoder _mediaEncoder; - public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager) + public ConfigurationService(IJsonSerializer jsonSerializer, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IProviderManager providerManager, ILibraryManager libraryManager, IMediaEncoder mediaEncoder) { _jsonSerializer = jsonSerializer; _configurationManager = configurationManager; _fileSystem = fileSystem; _providerManager = providerManager; _libraryManager = libraryManager; + _mediaEncoder = mediaEncoder; + } + + public void Post(UpdateMediaEncoderPath request) + { + var task = _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType); + Task.WaitAll(task); } /// <summary> diff --git a/MediaBrowser.Api/Dlna/DlnaService.cs b/MediaBrowser.Api/Dlna/DlnaService.cs index b6c4f5dfb..dd4b59b92 100644 --- a/MediaBrowser.Api/Dlna/DlnaService.cs +++ b/MediaBrowser.Api/Dlna/DlnaService.cs @@ -31,11 +31,9 @@ namespace MediaBrowser.Api.Dlna public string Id { get; set; } } - [Route("/Dlna/Profiles/{ProfileId}", "POST", Summary = "Updates a profile")] + [Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")] public class UpdateProfile : DeviceProfile, IReturnVoid { - [ApiMember(Name = "ProfileId", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string ProfileId { get; set; } } [Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")] @@ -89,4 +87,4 @@ namespace MediaBrowser.Api.Dlna _dlnaManager.CreateProfile(request); } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index 4e88e946f..b354cb26c 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -90,6 +90,17 @@ namespace MediaBrowser.Api public string Path { get; set; } } + public class DefaultDirectoryBrowserInfo + { + public string Path { get; set; } + } + + [Route("/Environment/DefaultDirectoryBrowser", "GET", Summary = "Gets the parent path of a given path")] + public class GetDefaultDirectoryBrowser : IReturn<DefaultDirectoryBrowserInfo> + { + + } + /// <summary> /// Class EnvironmentService /// </summary> @@ -108,7 +119,6 @@ namespace MediaBrowser.Api /// Initializes a new instance of the <see cref="EnvironmentService" /> class. /// </summary> /// <param name="networkManager">The network manager.</param> - /// <exception cref="System.ArgumentNullException">networkManager</exception> public EnvironmentService(INetworkManager networkManager, IFileSystem fileSystem) { if (networkManager == null) @@ -120,6 +130,29 @@ namespace MediaBrowser.Api _fileSystem = fileSystem; } + public object Get(GetDefaultDirectoryBrowser request) + { + var result = new DefaultDirectoryBrowserInfo(); + + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + try + { + var qnap = "/share/CACHEDEV1_DATA"; + if (Directory.Exists(qnap)) + { + result.Path = qnap; + } + } + catch + { + + } + } + + return ToOptimizedResult(result); + } + /// <summary> /// Gets the specified request. /// </summary> diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 6d1c5d868..b3b75359a 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -80,7 +80,7 @@ namespace MediaBrowser.Api .OrderBy(i => i) .ToArray(); - result.Tags = items.OfType<IHasTags>() + result.Tags = items .SelectMany(i => i.Tags) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(i => i) @@ -103,7 +103,8 @@ namespace MediaBrowser.Api User = user, MediaTypes = request.GetMediaTypes(), IncludeItemTypes = request.GetIncludeItemTypes(), - Recursive = true + Recursive = true, + EnableTotalRecordCount = false }; return query; diff --git a/MediaBrowser.Api/GamesService.cs b/MediaBrowser.Api/GamesService.cs index 040872fcc..0953b95e6 100644 --- a/MediaBrowser.Api/GamesService.cs +++ b/MediaBrowser.Api/GamesService.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api { @@ -107,8 +109,7 @@ namespace MediaBrowser.Api { IncludeItemTypes = new[] { typeof(GameSystem).Name } }; - var parentIds = new string[] { } ; - var gameSystems = _libraryManager.GetItemList(query, parentIds) + var gameSystems = _libraryManager.GetItemList(query) .Cast<GameSystem>() .ToList(); @@ -128,8 +129,7 @@ namespace MediaBrowser.Api { IncludeItemTypes = new[] { typeof(Game).Name } }; - var parentIds = new string[] { }; - var games = _libraryManager.GetItemList(query, parentIds) + var games = _libraryManager.GetItemList(query) .Cast<Game>() .ToList(); @@ -162,7 +162,10 @@ namespace MediaBrowser.Api var items = user == null ? system.GetRecursiveChildren(i => i is Game) : - system.GetRecursiveChildren(user, i => i is Game); + system.GetRecursiveChildren(user, new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Game).Name } + }); var games = items.Cast<Game>().ToList(); @@ -182,20 +185,42 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetSimilarGames request) + public async Task<object> Get(GetSimilarGames request) + { + var result = await GetSimilarItemsResult(request).ConfigureAwait(false); + + return ToOptimizedSerializedResultUsingCache(result); + } + + private async Task<QueryResult<BaseItemDto>> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request) { + 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); + + var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Limit = request.Limit, + IncludeItemTypes = new[] + { + typeof(Game).Name + }, + SimilarTo = item + + }).ToList(); + var dtoOptions = GetDtoOptions(request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, - _itemRepo, - _libraryManager, - _userDataRepository, - _dtoService, - Logger, - request, new[] { typeof(Game) }, - SimilarItemsHelper.GetSimiliarityScore); + var result = new QueryResult<BaseItemDto> + { + Items = (await _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user).ConfigureAwait(false)).ToArray(), - return ToOptimizedSerializedResultUsingCache(result); + TotalRecordCount = itemsResult.Count + }; + + return result; } } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index e5fe5bd68..5866ad15b 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -273,7 +273,9 @@ namespace MediaBrowser.Api.Images { var list = new List<ImageInfo>(); - foreach (var image in item.ImageInfos.Where(i => !item.AllowsMultipleImages(i.Type))) + var itemImages = item.ImageInfos; + + foreach (var image in itemImages.Where(i => !item.AllowsMultipleImages(i.Type))) { var info = GetImageInfo(item, image, null); @@ -283,14 +285,14 @@ namespace MediaBrowser.Api.Images } } - foreach (var imageType in item.ImageInfos.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) + foreach (var imageType in itemImages.Select(i => i.Type).Distinct().Where(item.AllowsMultipleImages)) { var index = 0; // Prevent implicitly captured closure var currentImageType = imageType; - foreach (var image in item.ImageInfos.Where(i => i.Type == currentImageType)) + foreach (var image in itemImages.Where(i => i.Type == currentImageType)) { var info = GetImageInfo(item, image, index); @@ -514,7 +516,7 @@ namespace MediaBrowser.Api.Images /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> /// <returns>System.Object.</returns> /// <exception cref="ResourceNotFoundException"></exception> - public object GetImage(ImageRequest request, IHasImages item, bool isHeadRequest) + public Task<object> GetImage(ImageRequest request, IHasImages item, bool isHeadRequest) { if (request.PercentPlayed.HasValue) { @@ -594,8 +596,7 @@ namespace MediaBrowser.Api.Images supportedImageEnhancers, cacheDuration, responseHeaders, - isHeadRequest) - .Result; + isHeadRequest); } private async Task<object> GetImageResult(IHasImages item, @@ -632,18 +633,20 @@ namespace MediaBrowser.Api.Images headers["Vary"] = "Accept"; - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { CacheDuration = cacheDuration, ResponseHeaders = headers, ContentType = imageResult.Item2, + DateLastModified = imageResult.Item3, IsHeadRequest = isHeadRequest, Path = imageResult.Item1, // Sometimes imagemagick keeps a hold on the file briefly even after it's done writing to it. // I'd rather do this than add a delay after saving the file FileShare = FileShare.ReadWrite - }); + + }).ConfigureAwait(false); } private List<ImageFormat> GetOutputFormats(ImageRequest request, ItemImageInfo image, bool cropwhitespace, List<IImageEnhancer> enhancers) diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index 02d1cdbe2..b21e54495 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -238,9 +238,9 @@ namespace MediaBrowser.Api.Images } if (_fileSystem.FileExists(contentPath)) - { - return ToStaticFileResult(contentPath); - } + { + return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); + } } catch (DirectoryNotFoundException) { @@ -259,9 +259,9 @@ namespace MediaBrowser.Api.Images contentPath = await reader.ReadToEndAsync().ConfigureAwait(false); } - return ToStaticFileResult(contentPath); + return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); } - + /// <summary> /// Downloads the image. /// </summary> diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index 8be37e210..357ff4394 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -38,6 +38,12 @@ namespace MediaBrowser.Api { } + [Route("/Items/RemoteSearch/Trailer", "POST")] + [Authenticated] + public class GetTrailerRemoteSearchResults : RemoteSearchQuery<TrailerInfo>, IReturn<List<RemoteSearchResult>> + { + } + [Route("/Items/RemoteSearch/AdultVideo", "POST")] [Authenticated] public class GetAdultVideoRemoteSearchResults : RemoteSearchQuery<ItemLookupInfo>, IReturn<List<RemoteSearchResult>> @@ -132,60 +138,65 @@ namespace MediaBrowser.Api return ToOptimizedResult(infos); } - public object Post(GetMovieRemoteSearchResults request) + public async Task<object> Post(GetTrailerRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetSeriesRemoteSearchResults request) + public async Task<object> Post(GetMovieRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetGameRemoteSearchResults request) + public async Task<object> Post(GetSeriesRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<Game, GameInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetBoxSetRemoteSearchResults request) + public async Task<object> Post(GetGameRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<Game, GameInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetPersonRemoteSearchResults request) + public async Task<object> Post(GetBoxSetRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetMusicAlbumRemoteSearchResults request) + public async Task<object> Post(GetPersonRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Post(GetMusicArtistRemoteSearchResults request) + public async Task<object> Post(GetMusicAlbumRemoteSearchResults request) { - var result = _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(request, CancellationToken.None).Result; + var result = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(request, CancellationToken.None).ConfigureAwait(false); return ToOptimizedResult(result); } - public object Get(GetRemoteSearchImage request) + public async Task<object> Post(GetMusicArtistRemoteSearchResults request) { - var result = GetRemoteImage(request).Result; + var result = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(request, CancellationToken.None).ConfigureAwait(false); - return result; + return ToOptimizedResult(result); + } + + public Task<object> Get(GetRemoteSearchImage request) + { + return GetRemoteImage(request); } public void Post(ApplySearchCriteria request) @@ -202,14 +213,19 @@ namespace MediaBrowser.Api // } //} Logger.Info("Setting provider id's to item {0}-{1}: {2}", item.Id, item.Name, _json.SerializeToString(request.ProviderIds)); + + // Since the refresh process won't erase provider Ids, we need to set this explicitly now. item.ProviderIds = request.ProviderIds; + //item.ProductionYear = request.ProductionYear; + //item.Name = request.Name; - var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(_fileSystem) + var task = _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(_fileSystem) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = ImageRefreshMode.FullRefresh, ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages + ReplaceAllImages = request.ReplaceAllImages, + SearchResult = request }, CancellationToken.None); Task.WaitAll(task); @@ -234,9 +250,9 @@ namespace MediaBrowser.Api contentPath = await reader.ReadToEndAsync().ConfigureAwait(false); } - if (_fileSystem.FileExists(contentPath)) + if (_fileSystem.FileExists(contentPath)) { - return ToStaticFileResult(contentPath); + return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); } } catch (DirectoryNotFoundException) @@ -256,7 +272,7 @@ namespace MediaBrowser.Api contentPath = await reader.ReadToEndAsync().ConfigureAwait(false); } - return ToStaticFileResult(contentPath); + return await ResultFactory.GetStaticFileResult(Request, contentPath).ConfigureAwait(false); } /// <summary> @@ -275,7 +291,7 @@ namespace MediaBrowser.Api var fullCachePath = GetFullCachePath(urlHash + "." + ext); - _fileSystem.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) { using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, true)) @@ -284,7 +300,7 @@ namespace MediaBrowser.Api } } - _fileSystem.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); using (var writer = new StreamWriter(pointerCachePath)) { await writer.WriteAsync(fullCachePath).ConfigureAwait(false); diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index f37874774..b944a39b6 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -70,26 +70,21 @@ namespace MediaBrowser.Api Cultures = _localizationManager.GetCultures().ToList() }; - var locationType = item.LocationType; - if (locationType == LocationType.FileSystem || - locationType == LocationType.Offline) + if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName)) { - if (!(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName)) + var inheritedContentType = _libraryManager.GetInheritedContentType(item); + var configuredContentType = _libraryManager.GetConfiguredContentType(item); + + if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || !string.IsNullOrWhiteSpace(configuredContentType)) { - var inheritedContentType = _libraryManager.GetInheritedContentType(item); - var configuredContentType = _libraryManager.GetConfiguredContentType(item); + info.ContentTypeOptions = GetContentTypeOptions(true); + info.ContentType = configuredContentType; - if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || !string.IsNullOrWhiteSpace(configuredContentType)) + if (string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) { - info.ContentTypeOptions = GetContentTypeOptions(true); - info.ContentType = configuredContentType; - - if (string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - { - info.ContentTypeOptions = info.ContentTypeOptions - .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } + info.ContentTypeOptions = info.ContentTypeOptions + .Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + .ToList(); } } } @@ -247,6 +242,12 @@ namespace MediaBrowser.Api hasBudget.Revenue = request.Revenue; } + var hasOriginalTitle = item as IHasOriginalTitle; + if (hasOriginalTitle != null) + { + hasOriginalTitle.OriginalTitle = hasOriginalTitle.OriginalTitle; + } + var hasCriticRating = item as IHasCriticRating; if (hasCriticRating != null) { @@ -274,11 +275,7 @@ namespace MediaBrowser.Api episode.AbsoluteEpisodeNumber = request.AbsoluteEpisodeNumber; } - var hasTags = item as IHasTags; - if (hasTags != null) - { - hasTags.Tags = request.Tags; - } + item.Tags = request.Tags; var hasTaglines = item as IHasTaglines; if (hasTaglines != null) @@ -292,11 +289,7 @@ namespace MediaBrowser.Api hasShortOverview.ShortOverview = request.ShortOverview; } - var hasKeywords = item as IHasKeywords; - if (hasKeywords != null) - { - hasKeywords.Keywords = request.Keywords; - } + item.Keywords = request.Keywords; if (request.Studios != null) { @@ -421,11 +414,6 @@ namespace MediaBrowser.Api series.Status = request.SeriesStatus; series.AirDays = request.AirDays; series.AirTime = request.AirTime; - - if (request.DisplaySpecialsWithSeasons.HasValue) - { - series.DisplaySpecialsWithSeasons = request.DisplaySpecialsWithSeasons.Value; - } } } diff --git a/MediaBrowser.Api/Library/FileOrganizationService.cs b/MediaBrowser.Api/Library/FileOrganizationService.cs index 849e9cf0d..0ed08a860 100644 --- a/MediaBrowser.Api/Library/FileOrganizationService.cs +++ b/MediaBrowser.Api/Library/FileOrganizationService.cs @@ -119,8 +119,6 @@ namespace MediaBrowser.Api.Library { private readonly IFileOrganizationService _iFileOrganizationService; - /// The _json serializer - /// </summary> private readonly IJsonSerializer _jsonSerializer; public FileOrganizationService(IFileOrganizationService iFileOrganizationService, IJsonSerializer jsonSerializer) diff --git a/MediaBrowser.Api/Library/LibraryHelpers.cs b/MediaBrowser.Api/Library/LibraryHelpers.cs deleted file mode 100644 index 46ec4f270..000000000 --- a/MediaBrowser.Api/Library/LibraryHelpers.cs +++ /dev/null @@ -1,89 +0,0 @@ -using MediaBrowser.Controller; -using System; -using System.IO; -using System.Linq; -using CommonIO; - -namespace MediaBrowser.Api.Library -{ - /// <summary> - /// Class LibraryHelpers - /// </summary> - public static class LibraryHelpers - { - /// <summary> - /// The shortcut file extension - /// </summary> - private const string ShortcutFileExtension = ".mblink"; - /// <summary> - /// The shortcut file search - /// </summary> - private const string ShortcutFileSearch = "*" + ShortcutFileExtension; - - /// <summary> - /// Deletes a shortcut from within a virtual folder, within either the default view or a user view - /// </summary> - /// <param name="fileSystem">The file system.</param> - /// <param name="virtualFolderName">Name of the virtual folder.</param> - /// <param name="mediaPath">The media path.</param> - /// <param name="appPaths">The app paths.</param> - /// <exception cref="System.IO.DirectoryNotFoundException">The media folder does not exist</exception> - public static void RemoveMediaPath(IFileSystem fileSystem, string virtualFolderName, string mediaPath, IServerApplicationPaths appPaths) - { - if (string.IsNullOrWhiteSpace(mediaPath)) - { - throw new ArgumentNullException("mediaPath"); - } - - var rootFolderPath = appPaths.DefaultUserViewsPath; - var path = Path.Combine(rootFolderPath, virtualFolderName); - - if (!fileSystem.DirectoryExists(path)) - { - throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); - } - - var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); - - if (!string.IsNullOrEmpty(shortcut)) - { - fileSystem.DeleteFile(shortcut); - } - } - - /// <summary> - /// Adds an additional mediaPath to an existing virtual folder, within either the default view or a user view - /// </summary> - /// <param name="fileSystem">The file system.</param> - /// <param name="virtualFolderName">Name of the virtual folder.</param> - /// <param name="path">The path.</param> - /// <param name="appPaths">The app paths.</param> - public static void AddMediaPath(IFileSystem fileSystem, string virtualFolderName, string path, IServerApplicationPaths appPaths) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentNullException("path"); - } - - if (!fileSystem.DirectoryExists(path)) - { - throw new DirectoryNotFoundException("The path does not exist."); - } - - var rootFolderPath = appPaths.DefaultUserViewsPath; - var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - - var shortcutFilename = fileSystem.GetFileNameWithoutExtension(path); - - var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - - while (fileSystem.FileExists(lnk)) - { - shortcutFilename += "1"; - lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - } - - fileSystem.CreateShortcut(lnk, path); - } - } -} diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index f9b3def97..14a771db0 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -350,7 +350,8 @@ namespace MediaBrowser.Api.Library Fields = request.Fields, Id = request.Id, Limit = request.Limit, - UserId = request.UserId + UserId = request.UserId, + ExcludeArtistIds = request.ExcludeArtistIds }); } if (item is MusicArtist) @@ -493,7 +494,7 @@ namespace MediaBrowser.Api.Library } } - public object Get(GetDownload request) + public Task<object> Get(GetDownload request) { var item = _libraryManager.GetItemById(request.Id); var auth = _authContext.GetAuthorizationInfo(Request); @@ -552,7 +553,7 @@ namespace MediaBrowser.Api.Library } } - public object Get(GetFile request) + public Task<object> Get(GetFile request) { var item = _libraryManager.GetItemById(request.Id); var locationType = item.LocationType; @@ -565,7 +566,7 @@ namespace MediaBrowser.Api.Library throw new ArgumentException("This command cannot be used for directories."); } - return ToStaticFileResult(item.Path); + return ResultFactory.GetStaticFileResult(Request, item.Path); } /// <summary> @@ -839,6 +840,7 @@ namespace MediaBrowser.Api.Library var dtoOptions = GetDtoOptions(request); var dtos = GetThemeSongIds(item).Select(_libraryManager.GetItemById) + .Where(i => i != null) .OrderBy(i => i.SortName) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); @@ -882,6 +884,7 @@ namespace MediaBrowser.Api.Library var dtoOptions = GetDtoOptions(request); var dtos = GetThemeVideoIds(item).Select(_libraryManager.GetItemById) + .Where(i => i != null) .OrderBy(i => i.SortName) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 244dcf09f..3cf0d5d93 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -190,75 +190,7 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Post(AddVirtualFolder request) { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException("request"); - } - - var name = _fileSystem.GetValidFilename(request.Name); - - var rootFolderPath = _appPaths.DefaultUserViewsPath; - - var virtualFolderPath = Path.Combine(rootFolderPath, name); - while (_fileSystem.DirectoryExists(virtualFolderPath)) - { - name += "1"; - virtualFolderPath = Path.Combine(rootFolderPath, name); - } - - if (request.Paths != null) - { - var invalidpath = request.Paths.FirstOrDefault(i => !_fileSystem.DirectoryExists(i)); - if (invalidpath != null) - { - throw new ArgumentException("The specified path does not exist: " + invalidpath + "."); - } - } - - _libraryMonitor.Stop(); - - try - { - _fileSystem.CreateDirectory(virtualFolderPath); - - if (!string.IsNullOrEmpty(request.CollectionType)) - { - var path = Path.Combine(virtualFolderPath, request.CollectionType + ".collection"); - - using (File.Create(path)) - { - - } - } - - if (request.Paths != null) - { - foreach (var path in request.Paths) - { - LibraryHelpers.AddMediaPath(_fileSystem, name, path, _appPaths); - } - } - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } + _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, request.RefreshLibrary); } /// <summary> @@ -336,46 +268,7 @@ namespace MediaBrowser.Api.Library /// <param name="request">The request.</param> public void Delete(RemoveVirtualFolder request) { - if (string.IsNullOrWhiteSpace(request.Name)) - { - throw new ArgumentNullException("request"); - } - - var rootFolderPath = _appPaths.DefaultUserViewsPath; - - var path = Path.Combine(rootFolderPath, request.Name); - - if (!_fileSystem.DirectoryExists(path)) - { - throw new DirectoryNotFoundException("The media folder does not exist"); - } - - _libraryMonitor.Stop(); - - try - { - _fileSystem.DeleteDirectory(path, true); - } - finally - { - Task.Run(() => - { - // No need to start if scanning the library because it will handle it - if (request.RefreshLibrary) - { - _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); - } - else - { - // Need to add a delay here or directory watchers may still pick up the changes - var task = Task.Delay(1000); - // Have to block here to allow exceptions to bubble - Task.WaitAll(task); - - _libraryMonitor.Start(); - } - }); - } + _libraryManager.RemoveVirtualFolder(request.Name, request.RefreshLibrary); } /// <summary> @@ -393,7 +286,7 @@ namespace MediaBrowser.Api.Library try { - LibraryHelpers.AddMediaPath(_fileSystem, request.Name, request.Path, _appPaths); + _libraryManager.AddMediaPath(request.Name, request.Path); } finally { @@ -432,7 +325,7 @@ namespace MediaBrowser.Api.Library try { - LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, _appPaths); + _libraryManager.RemoveMediaPath(request.Name, request.Path); } finally { diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 5b7bc78a8..c687758b7 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -146,6 +146,13 @@ namespace MediaBrowser.Api.LiveTv /// <value>The fields.</value> [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Fields { get; set; } + + public bool EnableTotalRecordCount { get; set; } + + public GetRecordings() + { + EnableTotalRecordCount = true; + } } [Route("/LiveTv/Recordings/Groups", "GET", Summary = "Gets live tv recording groups")] @@ -200,6 +207,8 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "SeriesTimerId", Description = "Optional filter by timers belonging to a series timer", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string SeriesTimerId { get; set; } + + public bool? IsActive { get; set; } } [Route("/LiveTv/Programs", "GET,POST", Summary = "Gets available live tv epgs..")] @@ -254,6 +263,8 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] public bool? EnableImages { get; set; } + public bool EnableTotalRecordCount { get; set; } + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? ImageTypeLimit { get; set; } @@ -266,12 +277,24 @@ namespace MediaBrowser.Api.LiveTv /// <value>The fields.</value> [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Fields { get; set; } + + public GetPrograms() + { + EnableTotalRecordCount = true; + } } [Route("/LiveTv/Programs/Recommended", "GET", Summary = "Gets available live tv epgs..")] [Authenticated] public class GetRecommendedPrograms : IReturn<QueryResult<BaseItemDto>>, IHasDtoOptions { + public bool EnableTotalRecordCount { get; set; } + + public GetRecommendedPrograms() + { + EnableTotalRecordCount = true; + } + [ApiMember(Name = "UserId", Description = "Optional filter by user id.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET,POST")] public string UserId { get; set; } @@ -425,6 +448,12 @@ namespace MediaBrowser.Api.LiveTv public string Id { get; set; } } + [Route("/LiveTv/ListingProviders/Default", "GET")] + [Authenticated(AllowBeforeStartupWizard = true)] + public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo> + { + } + [Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")] [Authenticated(AllowBeforeStartupWizard = true)] public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo> @@ -464,6 +493,32 @@ namespace MediaBrowser.Api.LiveTv { } + [Route("/LiveTv/ChannelMappingOptions")] + [Authenticated(AllowBeforeStartupWizard = true)] + public class GetChannelMappingOptions + { + [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ProviderId { get; set; } + } + + [Route("/LiveTv/ChannelMappings")] + [Authenticated(AllowBeforeStartupWizard = true)] + public class SetChannelMapping + { + [ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ProviderId { get; set; } + public string TunerChannelNumber { get; set; } + public string ProviderChannelNumber { get; set; } + } + + public class ChannelMappingOptions + { + public List<TunerChannelMapping> TunerChannels { get; set; } + public List<NameIdPair> ProviderChannels { get; set; } + public List<NameValuePair> Mappings { get; set; } + public string ProviderName { get; set; } + } + [Route("/LiveTv/Registration", "GET")] [Authenticated] public class GetLiveTvRegistrationInfo : IReturn<MBRegistrationRecord> @@ -482,7 +537,14 @@ namespace MediaBrowser.Api.LiveTv [Authenticated(AllowBeforeStartupWizard = true)] public class GetSatIniMappings : IReturn<List<NameValuePair>> { - + + } + + [Route("/LiveTv/TunerHosts/Satip/ChannelScan", "GET", Summary = "Scans for available channels")] + [Authenticated(AllowBeforeStartupWizard = true)] + public class GetSatChannnelScanResult : TunerHostInfo + { + } public class LiveTvService : BaseApiService @@ -504,6 +566,18 @@ namespace MediaBrowser.Api.LiveTv _dtoService = dtoService; } + public object Get(GetDefaultListingProvider request) + { + return ToOptimizedResult(new ListingsProviderInfo()); + } + + public async Task<object> Get(GetSatChannnelScanResult request) + { + var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + public async Task<object> Get(GetLiveTvRegistrationInfo request) { var result = await _liveTvManager.GetRegistrationInfo(request.ChannelId, request.ProgramId, request.Feature).ConfigureAwait(false); @@ -511,6 +585,46 @@ namespace MediaBrowser.Api.LiveTv return ToOptimizedResult(result); } + public async Task<object> Post(SetChannelMapping request) + { + return await _liveTvManager.SetChannelMapping(request.ProviderId, request.TunerChannelNumber, request.ProviderChannelNumber).ConfigureAwait(false); + } + + public async Task<object> Get(GetChannelMappingOptions request) + { + var config = GetConfiguration(); + + var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(request.ProviderId, i.Id, StringComparison.OrdinalIgnoreCase)); + + var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name; + + var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(request.ProviderId, CancellationToken.None) + .ConfigureAwait(false); + + var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(request.ProviderId, CancellationToken.None) + .ConfigureAwait(false); + + var mappings = listingsProviderInfo.ChannelMappings.ToList(); + + var result = new ChannelMappingOptions + { + TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(), + + ProviderChannels = providerChannels.Select(i => new NameIdPair + { + Name = i.Name, + Id = i.Number + + }).ToList(), + + Mappings = mappings, + + ProviderName = listingsProviderName + }; + + return ToOptimizedResult(result); + } + public object Get(GetSatIniMappings request) { return ToOptimizedResult(_liveTvManager.GetSatIniMappings()); @@ -522,9 +636,7 @@ namespace MediaBrowser.Api.LiveTv var response = await _httpClient.Get(new HttpRequestOptions { - Url = "https://json.schedulesdirect.org/20141201/available/countries", - CacheLength = TimeSpan.FromDays(1), - CacheMode = CacheMode.Unconditional + Url = "https://json.schedulesdirect.org/20141201/available/countries" }).ConfigureAwait(false); @@ -554,11 +666,7 @@ namespace MediaBrowser.Api.LiveTv public void Delete(DeleteListingProvider request) { - var config = GetConfiguration(); - - config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(request.Id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList(); - - _config.SaveConfiguration("livetv", config); + _liveTvManager.DeleteListingsProvider(request.Id); } public async Task<object> Post(AddTunerHost request) @@ -581,6 +689,11 @@ namespace MediaBrowser.Api.LiveTv return _config.GetConfiguration<LiveTvOptions>("livetv"); } + private void UpdateConfiguration(LiveTvOptions options) + { + _config.SaveConfiguration("livetv", options); + } + public async Task<object> Get(GetLineups request) { var info = await _liveTvManager.GetLineups(request.Type, request.Id, request.Country, request.Location).ConfigureAwait(false); @@ -613,14 +726,14 @@ namespace MediaBrowser.Api.LiveTv var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId); - var returnArray = _dtoService.GetBaseItemDtos(channelResult.Items, GetDtoOptions(Request), user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, GetDtoOptions(Request), user).ConfigureAwait(false)).ToArray(); var result = new QueryResult<BaseItemDto> { Items = returnArray, TotalRecordCount = channelResult.TotalRecordCount }; - + return ToOptimizedSerializedResultUsingCache(result); } @@ -648,7 +761,8 @@ namespace MediaBrowser.Api.LiveTv { ChannelIds = (request.ChannelIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToArray(), UserId = request.UserId, - HasAired = request.HasAired + HasAired = request.HasAired, + EnableTotalRecordCount = request.EnableTotalRecordCount }; if (!string.IsNullOrEmpty(request.MinStartDate)) @@ -695,7 +809,8 @@ namespace MediaBrowser.Api.LiveTv HasAired = request.HasAired, IsMovie = request.IsMovie, IsKids = request.IsKids, - IsSports = request.IsSports + IsSports = request.IsSports, + EnableTotalRecordCount = request.EnableTotalRecordCount }; var result = await _liveTvManager.GetRecommendedPrograms(query, GetDtoOptions(request), CancellationToken.None).ConfigureAwait(false); @@ -722,7 +837,8 @@ namespace MediaBrowser.Api.LiveTv Limit = request.Limit, Status = request.Status, SeriesTimerId = request.SeriesTimerId, - IsInProgress = request.IsInProgress + IsInProgress = request.IsInProgress, + EnableTotalRecordCount = request.EnableTotalRecordCount }, options, CancellationToken.None).ConfigureAwait(false); @@ -753,7 +869,8 @@ namespace MediaBrowser.Api.LiveTv var result = await _liveTvManager.GetTimers(new TimerQuery { ChannelId = request.ChannelId, - SeriesTimerId = request.SeriesTimerId + SeriesTimerId = request.SeriesTimerId, + IsActive = request.IsActive }, CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index cdc0cd6ae..77949d179 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -80,8 +80,6 @@ <Compile Include="FilterService.cs" /> <Compile Include="IHasDtoOptions.cs" /> <Compile Include="Library\ChapterService.cs" /> - <Compile Include="Playback\Dash\ManifestBuilder.cs" /> - <Compile Include="Playback\Dash\MpegDashService.cs" /> <Compile Include="Playback\MediaInfoService.cs" /> <Compile Include="Playback\TranscodingThrottler.cs" /> <Compile Include="PlaylistService.cs" /> @@ -131,7 +129,6 @@ <Compile Include="ItemUpdateService.cs" /> <Compile Include="Library\LibraryService.cs" /> <Compile Include="Library\FileOrganizationService.cs" /> - <Compile Include="Library\LibraryHelpers.cs" /> <Compile Include="Library\LibraryStructureService.cs" /> <Compile Include="LiveTv\LiveTvService.cs" /> <Compile Include="LocalizationService.cs" /> diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index e06b2c7f8..755713c84 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MediaBrowser.Controller.LiveTv; namespace MediaBrowser.Api.Movies { @@ -112,16 +113,14 @@ namespace MediaBrowser.Api.Movies /// <returns>System.Object.</returns> public async Task<object> Get(GetSimilarMovies request) { - var result = await GetSimilarItemsResult( - request, SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); + var result = await GetSimilarItemsResult(request).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } public async Task<object> Get(GetSimilarTrailers request) { - var result = await GetSimilarItemsResult( - request, SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); + var result = await GetSimilarItemsResult(request).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } @@ -130,152 +129,93 @@ namespace MediaBrowser.Api.Movies { var user = _userManager.GetUserById(request.UserId); - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { typeof(Movie).Name } - }; - - if (user.Configuration.IncludeTrailersInSuggestions) - { - var includeList = query.IncludeItemTypes.ToList(); - includeList.Add(typeof(Trailer).Name); - query.IncludeItemTypes = includeList.ToArray(); - } - - var parentIds = string.IsNullOrWhiteSpace(request.ParentId) ? new string[] { } : new[] { request.ParentId }; - var movies = _libraryManager.GetItemList(query, parentIds); - movies = _libraryManager.ReplaceVideosWithPrimaryVersions(movies); - - var listEligibleForCategories = new List<BaseItem>(); - var listEligibleForSuggestion = new List<BaseItem>(); - - var list = movies.ToList(); - - listEligibleForCategories.AddRange(list); - listEligibleForSuggestion.AddRange(list); - - listEligibleForCategories = listEligibleForCategories - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase) - .ToList(); - - listEligibleForSuggestion = listEligibleForSuggestion - .DistinctBy(i => i.Name, StringComparer.OrdinalIgnoreCase) - .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString(), StringComparer.OrdinalIgnoreCase) - .ToList(); - var dtoOptions = GetDtoOptions(request); dtoOptions.Fields = request.GetItemFields().ToList(); - var result = GetRecommendationCategories(user, listEligibleForCategories, listEligibleForSuggestion, request.CategoryLimit, request.ItemLimit, dtoOptions); + var result = GetRecommendationCategories(user, request.ParentId, request.CategoryLimit, request.ItemLimit, dtoOptions); return ToOptimizedResult(result); } - private async Task<ItemsResult> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore) + private async Task<QueryResult<BaseItemDto>> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request) { 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); - - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { typeof(Movie).Name } - }; - if (user == null || user.Configuration.IncludeTrailersInSuggestions) + var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { - var includeList = query.IncludeItemTypes.ToList(); - includeList.Add(typeof(Trailer).Name); - query.IncludeItemTypes = includeList.ToArray(); - } - - var parentIds = new string[] { }; - var list = _libraryManager.GetItemList(query, parentIds) - .Where(i => + Limit = request.Limit, + IncludeItemTypes = new[] { - // Strip out secondary versions - var v = i as Video; - return v != null && !v.PrimaryVersionId.HasValue; - }) - .DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N")) - .ToList(); - - if (item is Video) - { - var imdbId = item.GetProviderId(MetadataProviders.Imdb); - - // Use imdb id to try to filter duplicates of the same item - if (!string.IsNullOrWhiteSpace(imdbId)) - { - list = list - .Where(i => !string.Equals(imdbId, i.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - } - - var items = SimilarItemsHelper.GetSimilaritems(item, _libraryManager, list, getSimilarityScore).ToList(); + typeof(Movie).Name, + typeof(Trailer).Name, + typeof(LiveTvProgram).Name + }, + IsMovie = true, + SimilarTo = item, + EnableGroupByMetadataKey = true - IEnumerable<BaseItem> returnItems = items; - - if (request.Limit.HasValue) - { - returnItems = returnItems.Take(request.Limit.Value); - } + }).ToList(); var dtoOptions = GetDtoOptions(request); - var result = new ItemsResult + var result = new QueryResult<BaseItemDto> { - Items = _dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ToArray(), + Items = (await _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user).ConfigureAwait(false)).ToArray(), - TotalRecordCount = items.Count + TotalRecordCount = itemsResult.Count }; return result; } - private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, List<BaseItem> allMoviesForCategories, List<BaseItem> allMovies, int categoryLimit, int itemLimit, DtoOptions dtoOptions) + private IEnumerable<RecommendationDto> GetRecommendationCategories(User user, string parentId, int categoryLimit, int itemLimit, DtoOptions dtoOptions) { var categories = new List<RecommendationDto>(); - var recentlyPlayedMovies = allMoviesForCategories - .Select(i => - { - var userdata = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey()); - return new Tuple<BaseItem, bool, DateTime>(i, userdata.Played, userdata.LastPlayedDate ?? DateTime.MinValue); - }) - .Where(i => i.Item2) - .OrderByDescending(i => i.Item3) - .Select(i => i.Item1) - .ToList(); + var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? (Guid?)null : new Guid(parentId); - var excludeFromLiked = recentlyPlayedMovies.Take(10); - var likedMovies = allMovies - .Select(i => + var query = new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { - var score = 0; - var userData = _userDataRepository.GetUserData(user.Id, i.GetUserDataKey()); + typeof(Movie).Name, + //typeof(Trailer).Name, + //typeof(LiveTvProgram).Name + }, + // IsMovie = true + SortBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }, + SortOrder = SortOrder.Descending, + Limit = 7, + ParentId = parentIdGuid, + Recursive = true + }; - if (userData.IsFavorite) - { - score = 2; - } - else - { - score = userData.Likes.HasValue ? userData.Likes.Value ? 1 : -1 : 0; - } + var recentlyPlayedMovies = _libraryManager.GetItemList(query).ToList(); - return new Tuple<BaseItem, int>(i, score); - }) - .OrderByDescending(i => i.Item2) - .ThenBy(i => Guid.NewGuid()) - .Where(i => i.Item2 > 0) - .Select(i => i.Item1) - .Where(i => !excludeFromLiked.Contains(i)); + var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + IncludeItemTypes = new[] + { + typeof(Movie).Name, + typeof(Trailer).Name, + typeof(LiveTvProgram).Name + }, + IsMovie = true, + SortBy = new[] { ItemSortBy.Random }, + SortOrder = SortOrder.Descending, + Limit = 10, + IsFavoriteOrLiked = true, + ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id.ToString("N")).ToArray(), + EnableGroupByMetadataKey = true, + ParentId = parentIdGuid, + Recursive = true + + }).ToList(); var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList(); // Get recently played directors @@ -288,11 +228,11 @@ namespace MediaBrowser.Api.Movies .OrderBy(i => Guid.NewGuid()) .ToList(); - var similarToRecentlyPlayed = GetSimilarTo(user, allMovies, recentlyPlayedMovies.Take(7).OrderBy(i => Guid.NewGuid()), itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); - var similarToLiked = GetSimilarTo(user, allMovies, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); + var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator(); + var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator(); - var hasDirectorFromRecentlyPlayed = GetWithDirector(user, allMovies, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); - var hasActorFromRecentlyPlayed = GetWithActor(user, allMovies, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); + var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator(); + var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator(); var categoryTypes = new List<IEnumerator<RecommendationDto>> { @@ -335,44 +275,63 @@ namespace MediaBrowser.Api.Movies return categories.OrderBy(i => i.RecommendationType).ThenBy(i => Guid.NewGuid()); } - private IEnumerable<RecommendationDto> GetWithDirector(User user, List<BaseItem> allMovies, IEnumerable<string> directors, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable<RecommendationDto> GetWithDirector(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { - var userId = user.Id; - - foreach (var director in directors) + foreach (var name in names) { - var items = allMovies - .Where(i => _libraryManager.GetPeople(i).Any(p => string.Equals(p.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) && string.Equals(p.Name, director, StringComparison.OrdinalIgnoreCase))) - .Take(itemLimit) - .ToList(); + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Person = name, + // Account for duplicates by imdb id, since the database doesn't support this yet + Limit = itemLimit + 2, + PersonTypes = new[] { PersonType.Director }, + IncludeItemTypes = new[] + { + typeof(Movie).Name, + typeof(Trailer).Name, + typeof(LiveTvProgram).Name + }, + IsMovie = true, + EnableGroupByMetadataKey = true + + }).DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N")) + .Take(itemLimit) + .ToList(); if (items.Count > 0) { yield return new RecommendationDto { - BaselineItemName = director, - CategoryId = director.GetMD5().ToString("N"), + BaselineItemName = name, + CategoryId = name.GetMD5().ToString("N"), RecommendationType = type, - Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray() + Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).Result.ToArray() }; } } } - private IEnumerable<RecommendationDto> GetWithActor(User user, List<BaseItem> allMovies, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { foreach (var name in names) { - var itemsWithActor = _libraryManager.GetItemIds(new InternalItemsQuery(user) + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { - Person = name - - }); - - var items = allMovies - .Where(i => itemsWithActor.Contains(i.Id)) - .Take(itemLimit) - .ToList(); + Person = name, + // Account for duplicates by imdb id, since the database doesn't support this yet + Limit = itemLimit + 2, + IncludeItemTypes = new[] + { + typeof(Movie).Name, + typeof(Trailer).Name, + typeof(LiveTvProgram).Name + }, + IsMovie = true, + EnableGroupByMetadataKey = true + + }).DistinctBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N")) + .Take(itemLimit) + .ToList(); if (items.Count > 0) { @@ -381,20 +340,30 @@ namespace MediaBrowser.Api.Movies BaselineItemName = name, CategoryId = name.GetMD5().ToString("N"), RecommendationType = type, - Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).ToArray() + Items = _dtoService.GetBaseItemDtos(items, dtoOptions, user).Result.ToArray() }; } } } - private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<BaseItem> allMovies, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) + private IEnumerable<RecommendationDto> GetSimilarTo(User user, List<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type) { foreach (var item in baselineItems) { - var similar = SimilarItemsHelper - .GetSimilaritems(item, _libraryManager, allMovies, SimilarItemsHelper.GetSimiliarityScore) - .Take(itemLimit) - .ToList(); + var similar = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Limit = itemLimit, + IncludeItemTypes = new[] + { + typeof(Movie).Name, + typeof(Trailer).Name, + typeof(LiveTvProgram).Name + }, + IsMovie = true, + SimilarTo = item, + EnableGroupByMetadataKey = true + + }).ToList(); if (similar.Count > 0) { @@ -403,7 +372,7 @@ namespace MediaBrowser.Api.Movies BaselineItemName = item.Name, CategoryId = item.Id.ToString("N"), RecommendationType = type, - Items = _dtoService.GetBaseItemDtos(similar, dtoOptions, user).ToArray() + Items = _dtoService.GetBaseItemDtos(similar, dtoOptions, user).Result.ToArray() }; } } diff --git a/MediaBrowser.Api/Movies/TrailersService.cs b/MediaBrowser.Api/Movies/TrailersService.cs index d74dd5b6a..c70620cf9 100644 --- a/MediaBrowser.Api/Movies/TrailersService.cs +++ b/MediaBrowser.Api/Movies/TrailersService.cs @@ -58,7 +58,7 @@ namespace MediaBrowser.Api.Movies getItems.IncludeItemTypes = "Trailer"; - return new ItemsService(_userManager, _libraryManager, _userDataRepository, _localizationManager, _dtoService, _collectionManager) + return new ItemsService(_userManager, _libraryManager, _localizationManager, _dtoService) { AuthorizationContext = AuthorizationContext, Logger = Logger, diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index e774c3077..2d7e909bf 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -8,6 +8,7 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace MediaBrowser.Api.Music { @@ -49,18 +50,18 @@ namespace MediaBrowser.Api.Music _dtoService = dtoService; } - public object Get(GetSimilarArtists request) + public async Task<object> Get(GetSimilarArtists request) { var dtoOptions = GetDtoOptions(request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, + var result = await SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, _itemRepo, _libraryManager, _userDataRepository, _dtoService, Logger, request, new[] { typeof(MusicArtist) }, - SimilarItemsHelper.GetSimiliarityScore); + SimilarItemsHelper.GetSimiliarityScore).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } @@ -70,18 +71,18 @@ namespace MediaBrowser.Api.Music /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetSimilarAlbums request) + public async Task<object> Get(GetSimilarAlbums request) { var dtoOptions = GetDtoOptions(request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, + var result = await SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, _itemRepo, _libraryManager, _userDataRepository, _dtoService, Logger, request, new[] { typeof(MusicAlbum) }, - GetAlbumSimilarityScore); + GetAlbumSimilarityScore).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); } diff --git a/MediaBrowser.Api/Music/InstantMixService.cs b/MediaBrowser.Api/Music/InstantMixService.cs index d2a4aa60c..19265408b 100644 --- a/MediaBrowser.Api/Music/InstantMixService.cs +++ b/MediaBrowser.Api/Music/InstantMixService.cs @@ -8,6 +8,7 @@ using MediaBrowser.Model.Querying; using ServiceStack; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace MediaBrowser.Api.Music { @@ -76,7 +77,7 @@ namespace MediaBrowser.Api.Music _libraryManager = libraryManager; } - public object Get(GetInstantMixFromItem request) + public Task<object> Get(GetInstantMixFromItem request) { var item = _libraryManager.GetItemById(request.Id); @@ -87,7 +88,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromArtistId request) + public Task<object> Get(GetInstantMixFromArtistId request) { var item = _libraryManager.GetItemById(request.Id); @@ -98,7 +99,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromMusicGenreId request) + public Task<object> Get(GetInstantMixFromMusicGenreId request) { var item = _libraryManager.GetItemById(request.Id); @@ -109,7 +110,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromSong request) + public Task<object> Get(GetInstantMixFromSong request) { var item = _libraryManager.GetItemById(request.Id); @@ -120,7 +121,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromAlbum request) + public Task<object> Get(GetInstantMixFromAlbum request) { var album = _libraryManager.GetItemById(request.Id); @@ -131,7 +132,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromPlaylist request) + public Task<object> Get(GetInstantMixFromPlaylist request) { var playlist = (Playlist)_libraryManager.GetItemById(request.Id); @@ -142,7 +143,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromMusicGenre request) + public Task<object> Get(GetInstantMixFromMusicGenre request) { var user = _userManager.GetUserById(request.UserId); @@ -151,7 +152,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - public object Get(GetInstantMixFromArtist request) + public Task<object> Get(GetInstantMixFromArtist request) { var user = _userManager.GetUserById(request.UserId); var artist = _libraryManager.GetArtist(request.Name); @@ -161,7 +162,7 @@ namespace MediaBrowser.Api.Music return GetResult(items, user, request); } - private object GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request) + private async Task<object> GetResult(IEnumerable<Audio> items, User user, BaseGetSimilarItems request) { var list = items.ToList(); @@ -172,7 +173,7 @@ namespace MediaBrowser.Api.Music var dtoOptions = GetDtoOptions(request); - result.Items = _dtoService.GetBaseItemDtos(list.Take(request.Limit ?? list.Count), dtoOptions, user).ToArray(); + result.Items = (await _dtoService.GetBaseItemDtos(list.Take(request.Limit ?? list.Count), dtoOptions, user).ConfigureAwait(false)).ToArray(); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Api/PackageReviewService.cs b/MediaBrowser.Api/PackageReviewService.cs index e70d6a713..0a5b9bef8 100644 --- a/MediaBrowser.Api/PackageReviewService.cs +++ b/MediaBrowser.Api/PackageReviewService.cs @@ -112,7 +112,7 @@ namespace MediaBrowser.Api _appHost = appHost; } - public object Get(ReviewRequest request) + public async Task<object> Get(ReviewRequest request) { var parms = "?id=" + request.Id; @@ -133,11 +133,13 @@ namespace MediaBrowser.Api parms += "&title=true"; } - var result = _httpClient.Get(MbAdminUrl + "/service/packageReview/retrieve" + parms, CancellationToken.None).Result; - - var reviews = _serializer.DeserializeFromStream<List<PackageReviewInfo>>(result); + using (var result = await _httpClient.Get(MbAdminUrl + "/service/packageReview/retrieve" + parms, CancellationToken.None) + .ConfigureAwait(false)) + { + var reviews = _serializer.DeserializeFromStream<List<PackageReviewInfo>>(result); - return ToOptimizedResult(reviews); + return ToOptimizedResult(reviews); + } } public void Post(CreateReviewRequest request) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 44bc95ed1..ce3691095 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -286,13 +286,25 @@ namespace MediaBrowser.Api.Playback protected string GetH264Encoder(StreamState state) { - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + // Only use alternative encoders for video files. + // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully + // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + if (state.VideoType == VideoType.VideoFile) { - // It's currently failing on live tv - if (state.RunTimeTicks.HasValue) + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { return "h264_qsv"; } + + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase)) + { + return "h264_nvenc"; + } + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + return "h264_omx"; + } } return "libx264"; @@ -332,10 +344,10 @@ namespace MediaBrowser.Api.Playback } - // h264 (libnvenc) - else if (string.Equals(videoCodec, "libnvenc", StringComparison.OrdinalIgnoreCase)) + // h264 (h264_nvenc) + else if (string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) { - param = "-preset high-performance"; + param = "-preset default"; } // webm @@ -397,15 +409,18 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrEmpty(state.VideoRequest.Profile)) { - param += " -profile:v " + state.VideoRequest.Profile; + if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) + { + // not supported by h264_omx + param += " -profile:v " + state.VideoRequest.Profile; + } } if (!string.IsNullOrEmpty(state.VideoRequest.Level)) { - var h264Encoder = GetH264Encoder(state); - - // 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)) + // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format + if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) { switch (state.VideoRequest.Level) { @@ -441,13 +456,20 @@ namespace MediaBrowser.Api.Playback break; } } - else + else if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase)) { param += " -level " + state.VideoRequest.Level; } } - return "-pix_fmt yuv420p " + param; + if (!string.Equals(videoCodec, "h264_omx", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) && + !string.Equals(videoCodec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + { + param = "-pix_fmt yuv420p " + param; + } + + return param; } protected string GetAudioFilterParam(StreamState state, bool isHls) @@ -460,7 +482,7 @@ namespace MediaBrowser.Api.Playback // Boost volume to 200% when downsampling from 6ch to 2ch if (channels.HasValue && channels.Value <= 2) { - if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5) + if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !ApiEntryPoint.Instance.GetEncodingOptions().DownMixAudioBoost.Equals(1)) { volParam = ",volume=" + ApiEntryPoint.Instance.GetEncodingOptions().DownMixAudioBoost.ToString(UsCulture); } @@ -563,14 +585,6 @@ namespace MediaBrowser.Api.Playback 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 && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode) @@ -614,7 +628,7 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) { - var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.MediaSource.Protocol, CancellationToken.None).Result; + var charenc = SubtitleEncoder.GetSubtitleFileCharacterSet(subtitlePath, state.SubtitleStream.Language, state.MediaSource.Protocol, CancellationToken.None).Result; if (!string.IsNullOrEmpty(charenc)) { @@ -712,15 +726,16 @@ namespace MediaBrowser.Api.Playback inputChannels = null; } + int? resultChannels = null; var codec = outputAudioCodec ?? string.Empty; if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) { // wmav2 currently only supports two channel output - return Math.Min(2, inputChannels ?? 2); + resultChannels = Math.Min(2, inputChannels ?? 2); } - if (request.MaxAudioChannels.HasValue) + else if (request.MaxAudioChannels.HasValue) { var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1 ? 2 @@ -732,10 +747,18 @@ namespace MediaBrowser.Api.Playback } // 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); + resultChannels = Math.Min(request.MaxAudioChannels.Value, channelLimit); } - return request.AudioChannels; + if (resultChannels.HasValue && !string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (request.TranscodingMaxAudioChannels.HasValue) + { + resultChannels = Math.Min(request.TranscodingMaxAudioChannels.Value, resultChannels.Value); + } + } + + return resultChannels ?? request.AudioChannels; } /// <summary> @@ -821,9 +844,22 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected string GetVideoDecoder(StreamState state) { - if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Only use alternative encoders for video files. + // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully + // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. + if (state.VideoType != VideoType.VideoFile) + { + return null; + } + + if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec)) { - if (state.VideoStream != null && !string.IsNullOrWhiteSpace(state.VideoStream.Codec)) + if (string.Equals(ApiEntryPoint.Instance.GetEncodingOptions().HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { switch (state.MediaSource.VideoStream.Codec.ToLower()) { @@ -831,6 +867,7 @@ namespace MediaBrowser.Api.Playback case "h264": if (MediaEncoder.SupportsDecoder("h264_qsv")) { + // Seeing stalls and failures with decoding. Not worth it compared to encoding. return "-c:v h264_qsv "; } break; @@ -947,14 +984,24 @@ namespace MediaBrowser.Api.Playback await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false); - var transcodingId = Guid.NewGuid().ToString("N"); - var commandLineArgs = GetCommandLineArguments(outputPath, state, true); - - if (ApiEntryPoint.Instance.GetEncodingOptions().EnableDebugLogging) + if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - commandLineArgs = "-loglevel debug " + commandLineArgs; + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + if (!string.IsNullOrWhiteSpace(auth.UserId)) + { + var user = UserManager.GetUserById(auth.UserId); + if (!user.Policy.EnableVideoPlaybackTranscoding) + { + ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state); + + throw new ArgumentException("User does not have access to video transcoding"); + } + } } + var transcodingId = Guid.NewGuid().ToString("N"); + var commandLineArgs = GetCommandLineArguments(outputPath, state, true); + var process = new Process { StartInfo = new ProcessStartInfo @@ -963,7 +1010,7 @@ namespace MediaBrowser.Api.Playback UseShellExecute = false, // Must consume both stdout and stderr or deadlocks may occur - RedirectStandardOutput = true, + //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, @@ -995,7 +1042,17 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; Logger.Info(commandLineLogMessage); - var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, "transcode-" + Guid.NewGuid() + ".txt"); + var logFilePrefix = "transcode"; + if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + logFilePrefix = "directstream"; + } + else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + logFilePrefix = "remux"; + } + + var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + 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. @@ -1020,13 +1077,13 @@ namespace MediaBrowser.Api.Playback } // MUST read both stdout and stderr asynchronously or a deadlock may occurr - process.BeginOutputReadLine(); + //process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream); + Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream)); // Wait for the file to exist before proceeeding - while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) + while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } @@ -1066,7 +1123,7 @@ namespace MediaBrowser.Api.Playback return true; } - private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) + private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) { try { @@ -1172,7 +1229,7 @@ namespace MediaBrowser.Api.Playback } } - private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream) + private int? GetVideoBitrateParamValue(VideoStreamRequest request, MediaStream videoStream, string outputVideoCodec) { var bitrate = request.VideoBitRate; @@ -1197,6 +1254,18 @@ namespace MediaBrowser.Api.Playback } } + if (bitrate.HasValue) + { + var inputVideoCodec = videoStream == null ? null : videoStream.Codec; + bitrate = ResolutionNormalizer.ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); + + // If a max bitrate was requested, don't let the scaled bitrate exceed it + if (request.VideoBitRate.HasValue) + { + bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + } + } + return bitrate; } @@ -1452,10 +1521,7 @@ namespace MediaBrowser.Api.Playback } else if (i == 19) { - if (videoRequest != null) - { - videoRequest.Cabac = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } + // cabac no longer used } else if (i == 20) { @@ -1482,6 +1548,13 @@ namespace MediaBrowser.Api.Playback } else if (i == 25) { + if (videoRequest != null) + { + videoRequest.ForceLiveStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + } + else if (i == 26) + { if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) { SubtitleDeliveryMethod method; @@ -1491,6 +1564,17 @@ namespace MediaBrowser.Api.Playback } } } + else if (i == 27) + { + request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); + } + else if (i == 28) + { + if (videoRequest != null) + { + videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + } } } @@ -1582,7 +1666,8 @@ namespace MediaBrowser.Api.Playback var state = new StreamState(MediaSourceManager, Logger) { Request = request, - RequestedUrl = url + RequestedUrl = url, + UserAgent = Request.UserAgent }; //if ((Request.UserAgent ?? string.Empty).IndexOf("iphone", StringComparison.OrdinalIgnoreCase) != -1 || @@ -1595,7 +1680,8 @@ namespace MediaBrowser.Api.Playback if (!string.IsNullOrWhiteSpace(request.AudioCodec)) { state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(); + state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => MediaEncoder.CanEncodeToAudioCodec(i)) + ?? state.SupportedAudioCodecs.FirstOrDefault(); } var item = LibraryManager.GetItemById(request.Id); @@ -1649,14 +1735,14 @@ namespace MediaBrowser.Api.Playback if (videoRequest != null) { state.OutputVideoCodec = state.VideoRequest.VideoCodec; - state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream); + state.OutputVideoBitrate = GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); 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, videoRequest.MaxWidth, videoRequest.MaxHeight); @@ -1680,12 +1766,25 @@ namespace MediaBrowser.Api.Playback private void TryStreamCopy(StreamState state, VideoStreamRequest videoRequest) { - if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream)) + if (state.VideoStream != null && CanStreamCopyVideo(state)) { state.OutputVideoCodec = "copy"; } + else + { + // If the user doesn't have access to transcoding, then force stream copy, regardless of whether it will be compatible or not + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + if (!string.IsNullOrWhiteSpace(auth.UserId)) + { + var user = UserManager.GetUserById(auth.UserId); + if (!user.Policy.EnableVideoPlaybackTranscoding) + { + state.OutputVideoCodec = "copy"; + } + } + } - if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs)) + if (state.AudioStream != null && CanStreamCopyAudio(state, state.SupportedAudioCodecs)) { state.OutputAudioCodec = "copy"; } @@ -1773,8 +1872,11 @@ namespace MediaBrowser.Api.Playback state.MediaSource = mediaSource; } - protected virtual bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream) + protected virtual bool CanStreamCopyVideo(StreamState state) { + var request = state.VideoRequest; + var videoStream = state.VideoStream; + if (videoStream.IsInterlaced) { return false; @@ -1784,7 +1886,7 @@ namespace MediaBrowser.Api.Playback { return false; } - + // Can't stream copy if we're burning in subtitles if (request.SubtitleStreamIndex.HasValue) { @@ -1794,6 +1896,15 @@ namespace MediaBrowser.Api.Playback } } + if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + { + if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value) + { + Logger.Debug("Cannot stream copy video. Stream is marked as not AVC"); + return false; + } + } + // Source and target codecs must match if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase)) { @@ -1805,10 +1916,10 @@ namespace MediaBrowser.Api.Playback { if (string.IsNullOrEmpty(videoStream.Profile)) { - return false; + //return false; } - if (!string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(videoStream.Profile) && !string.Equals(request.Profile, videoStream.Profile, StringComparison.OrdinalIgnoreCase)) { var currentScore = GetVideoProfileScore(videoStream.Profile); var requestedScore = GetVideoProfileScore(request.Profile); @@ -1884,24 +1995,16 @@ namespace MediaBrowser.Api.Playback { if (!videoStream.Level.HasValue) { - return false; + //return false; } - if (videoStream.Level.Value > requestLevel) + if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) { return false; } } } - if (request.Cabac.HasValue && request.Cabac.Value) - { - if (videoStream.IsCabac.HasValue && !videoStream.IsCabac.Value) - { - return false; - } - } - return request.EnableAutoStreamCopy; } @@ -1921,8 +2024,11 @@ namespace MediaBrowser.Api.Playback return Array.FindIndex(list.ToArray(), t => string.Equals(t, profile, StringComparison.OrdinalIgnoreCase)); } - protected virtual bool CanStreamCopyAudio(VideoStreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs) + protected virtual bool CanStreamCopyAudio(StreamState state, List<string> supportedAudioCodecs) { + var request = state.VideoRequest; + var audioStream = state.AudioStream; + // Source and target codecs must match if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) { @@ -2028,7 +2134,6 @@ namespace MediaBrowser.Api.Playback state.TargetPacketLength, state.TargetTimestamp, state.IsTargetAnamorphic, - state.IsTargetCabac, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, @@ -2054,6 +2159,8 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null) { state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps; + state.VideoRequest.ForceLiveStream = transcodingProfile.ForceLiveStream; + state.VideoRequest.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; } } } @@ -2131,7 +2238,6 @@ namespace MediaBrowser.Api.Playback state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic, - state.IsTargetCabac, state.TargetRefFrames, state.TargetVideoStreamCount, state.TargetAudioStreamCount, @@ -2218,12 +2324,13 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null) { + // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase) && state.VideoRequest.CopyTimestamps) { - inputModifier += " -noaccurate_seek"; + //inputModifier += " -noaccurate_seek"; } } - + return inputModifier; } diff --git a/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs b/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs deleted file mode 100644 index 35e252a19..000000000 --- a/MediaBrowser.Api/Playback/Dash/ManifestBuilder.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Globalization; -using System.Security; -using System.Text; - -namespace MediaBrowser.Api.Playback.Dash -{ - public class ManifestBuilder - { - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - public string GetManifestText(StreamState state, string playlistUrl) - { - var builder = new StringBuilder(); - - var time = TimeSpan.FromTicks(state.RunTimeTicks.Value); - - var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S"; - - builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); - - builder.AppendFormat( - "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" type=\"static\" mediaPresentationDuration=\"{0}\" minBufferTime=\"PT5.0S\">", - duration); - - builder.Append("<ProgramInformation>"); - builder.Append("</ProgramInformation>"); - - builder.Append("<Period start=\"PT0S\">"); - builder.Append(GetVideoAdaptationSet(state, playlistUrl)); - builder.Append(GetAudioAdaptationSet(state, playlistUrl)); - builder.Append("</Period>"); - - builder.Append("</MPD>"); - - return builder.ToString(); - } - - private string GetVideoAdaptationSet(StreamState state, string playlistUrl) - { - var builder = new StringBuilder(); - - builder.Append("<AdaptationSet id=\"video\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">"); - builder.Append(GetVideoRepresentationOpenElement(state)); - - AppendSegmentList(state, builder, "0", playlistUrl); - - builder.Append("</Representation>"); - builder.Append("</AdaptationSet>"); - - return builder.ToString(); - } - - private string GetAudioAdaptationSet(StreamState state, string playlistUrl) - { - var builder = new StringBuilder(); - - builder.Append("<AdaptationSet id=\"audio\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">"); - builder.Append(GetAudioRepresentationOpenElement(state)); - - builder.Append("<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"6\" />"); - - AppendSegmentList(state, builder, "1", playlistUrl); - - builder.Append("</Representation>"); - builder.Append("</AdaptationSet>"); - - return builder.ToString(); - } - - private string GetVideoRepresentationOpenElement(StreamState state) - { - var codecs = GetVideoCodecDescriptor(state); - - var mime = "video/mp4"; - - var xml = "<Representation id=\"0\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\""; - - if (state.OutputWidth.HasValue) - { - xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\""; - } - if (state.OutputHeight.HasValue) - { - xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\""; - } - if (state.OutputVideoBitrate.HasValue) - { - xml += " bandwidth=\"" + state.OutputVideoBitrate.Value.ToString(UsCulture) + "\""; - } - - xml += ">"; - - return xml; - } - - private string GetAudioRepresentationOpenElement(StreamState state) - { - var codecs = GetAudioCodecDescriptor(state); - - var mime = "audio/mp4"; - - var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\""; - - if (state.OutputAudioSampleRate.HasValue) - { - xml += " audioSamplingRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\""; - } - if (state.OutputAudioBitrate.HasValue) - { - xml += " bandwidth=\"" + state.OutputAudioBitrate.Value.ToString(UsCulture) + "\""; - } - - xml += ">"; - - return xml; - } - - private string GetVideoCodecDescriptor(StreamState state) - { - // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html - // http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/ - - var level = state.TargetVideoLevel ?? 0; - var profile = state.TargetVideoProfile ?? string.Empty; - - if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1) - { - if (level >= 4.1) - { - return "avc1.640028"; - } - - if (level >= 4) - { - return "avc1.640028"; - } - - return "avc1.64001f"; - } - - if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1) - { - if (level >= 4) - { - return "avc1.4d0028"; - } - - if (level >= 3.1) - { - return "avc1.4d001f"; - } - - return "avc1.4d001e"; - } - - if (level >= 3.1) - { - return "avc1.42001f"; - } - - return "avc1.42E01E"; - } - - private string GetAudioCodecDescriptor(StreamState state) - { - // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html - - if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) - { - return "mp4a.40.34"; - } - - // AAC 5ch - if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5) - { - return "mp4a.40.5"; - } - - // AAC 2ch - return "mp4a.40.2"; - } - - private void AppendSegmentList(StreamState state, StringBuilder builder, string type, string playlistUrl) - { - var extension = ".m4s"; - - var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds; - - var queryStringIndex = playlistUrl.IndexOf('?'); - var queryString = queryStringIndex == -1 ? string.Empty : playlistUrl.Substring(queryStringIndex); - - var index = 0; - var duration = 1000000 * state.SegmentLength; - builder.AppendFormat("<SegmentList timescale=\"1000000\" duration=\"{0}\" startNumber=\"1\">", duration.ToString(CultureInfo.InvariantCulture)); - - while (seconds > 0) - { - var filename = index == 0 - ? "init" - : (index - 1).ToString(UsCulture); - - var segmentUrl = string.Format("dash/{3}/{0}{1}{2}", - filename, - extension, - SecurityElement.Escape(queryString), - type); - - if (index == 0) - { - builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl); - } - else - { - builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl); - } - - seconds -= state.SegmentLength; - index++; - } - builder.Append("</SegmentList>"); - } - } -} diff --git a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs b/MediaBrowser.Api/Playback/Dash/MpegDashService.cs deleted file mode 100644 index a35d13c5b..000000000 --- a/MediaBrowser.Api/Playback/Dash/MpegDashService.cs +++ /dev/null @@ -1,547 +0,0 @@ -using MediaBrowser.Api.Playback.Hls; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using ServiceStack; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommonIO; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace MediaBrowser.Api.Playback.Dash -{ - /// <summary> - /// Options is needed for chromecast. Threw Head in there since it's related - /// </summary> - [Route("/Videos/{Id}/master.mpd", "GET", Summary = "Gets a video stream using Mpeg dash.")] - [Route("/Videos/{Id}/master.mpd", "HEAD", Summary = "Gets a video stream using Mpeg dash.")] - public class GetMasterManifest : VideoStreamRequest - { - public bool EnableAdaptiveBitrateStreaming { get; set; } - - public GetMasterManifest() - { - EnableAdaptiveBitrateStreaming = true; - } - } - - [Route("/Videos/{Id}/dash/{RepresentationId}/{SegmentId}.m4s", "GET")] - public class GetDashSegment : VideoStreamRequest - { - /// <summary> - /// Gets or sets the segment id. - /// </summary> - /// <value>The segment id.</value> - public string SegmentId { get; set; } - - /// <summary> - /// Gets or sets the representation identifier. - /// </summary> - /// <value>The representation identifier.</value> - public string RepresentationId { get; set; } - } - - public class MpegDashService : BaseHlsService - { - public MpegDashService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer) - { - NetworkManager = networkManager; - } - - protected INetworkManager NetworkManager { get; private set; } - - public object Get(GetMasterManifest request) - { - var result = GetAsync(request, "GET").Result; - - return result; - } - - public object Head(GetMasterManifest request) - { - var result = GetAsync(request, "HEAD").Result; - - return result; - } - - protected override bool EnableOutputInSubFolder - { - get - { - return true; - } - } - - private async Task<object> GetAsync(GetMasterManifest request, string method) - { - if (string.IsNullOrEmpty(request.MediaSourceId)) - { - throw new ArgumentException("MediaSourceId is required"); - } - - var state = await GetState(request, CancellationToken.None).ConfigureAwait(false); - - var playlistText = string.Empty; - - if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) - { - playlistText = new ManifestBuilder().GetManifestText(state, Request.RawUrl); - } - - return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.mpd"), new Dictionary<string, string>()); - } - - public object Get(GetDashSegment request) - { - return GetDynamicSegment(request, request.SegmentId, request.RepresentationId).Result; - } - - private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId, string representationId) - { - if ((request.StartTimeTicks ?? 0) > 0) - { - throw new ArgumentException("StartTimeTicks is not allowed."); - } - - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; - - var requestedIndex = string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase) ? - -1 : - int.Parse(segmentId, NumberStyles.Integer, UsCulture); - - var state = await GetState(request, cancellationToken).ConfigureAwait(false); - - var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".mpd"); - - var segmentExtension = GetSegmentFileExtension(state); - - var segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); - var segmentLength = state.SegmentLength; - - TranscodingJob job = null; - - if (!string.IsNullOrWhiteSpace(segmentPath)) - { - job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); - } - - await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); - try - { - segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); - if (!string.IsNullOrWhiteSpace(segmentPath)) - { - job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false); - } - else - { - if (string.Equals(representationId, "0", StringComparison.OrdinalIgnoreCase)) - { - job = ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType); - var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); - var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; - Logger.Debug("Current transcoding index is {0}. requestedIndex={1}. segmentGapRequiringTranscodingChange={2}", currentTranscodingIndex ?? -2, requestedIndex, segmentGapRequiringTranscodingChange); - if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange) - { - // If the playlist doesn't already exist, startup ffmpeg - try - { - ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false); - - if (currentTranscodingIndex.HasValue) - { - DeleteLastTranscodedFiles(playlistPath, 0); - } - - var positionTicks = GetPositionTicks(state, requestedIndex); - request.StartTimeTicks = positionTicks; - - var startNumber = GetStartNumber(state); - - var workingDirectory = Path.Combine(Path.GetDirectoryName(playlistPath), (startNumber == -1 ? 0 : startNumber).ToString(CultureInfo.InvariantCulture)); - state.WaitForPath = Path.Combine(workingDirectory, Path.GetFileName(playlistPath)); - FileSystem.CreateDirectory(workingDirectory); - job = await StartFfMpeg(state, playlistPath, cancellationTokenSource, workingDirectory).ConfigureAwait(false); - await WaitForMinimumDashSegmentCount(Path.Combine(workingDirectory, Path.GetFileName(playlistPath)), 1, cancellationTokenSource.Token).ConfigureAwait(false); - } - catch - { - state.Dispose(); - throw; - } - } - } - } - } - finally - { - ApiEntryPoint.Instance.TranscodingStartLock.Release(); - } - - while (string.IsNullOrWhiteSpace(segmentPath)) - { - segmentPath = FindSegment(playlistPath, representationId, segmentExtension, requestedIndex); - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } - - Logger.Info("returning {0}", segmentPath); - return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job ?? ApiEntryPoint.Instance.GetTranscodingJob(playlistPath, TranscodingJobType), cancellationToken).ConfigureAwait(false); - } - - private long GetPositionTicks(StreamState state, int requestedIndex) - { - if (requestedIndex <= 0) - { - return 0; - } - - var startSeconds = requestedIndex * state.SegmentLength; - return TimeSpan.FromSeconds(startSeconds).Ticks; - } - - protected Task WaitForMinimumDashSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) - { - return WaitForSegment(playlist, "stream0-" + segmentCount.ToString("00000", CultureInfo.InvariantCulture) + ".m4s", cancellationToken); - } - - private async Task<object> GetSegmentResult(string playlistPath, - string segmentPath, - int segmentIndex, - int segmentLength, - TranscodingJob transcodingJob, - CancellationToken cancellationToken) - { - // If all transcoding has completed, just return immediately - if (transcodingJob != null && transcodingJob.HasExited) - { - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); - } - - // Wait for the file to stop being written to, then stream it - var length = new FileInfo(segmentPath).Length; - var eofCount = 0; - - while (eofCount < 10) - { - var info = new FileInfo(segmentPath); - - if (!info.Exists) - { - break; - } - - var newLength = info.Length; - - if (newLength == length) - { - eofCount++; - } - else - { - eofCount = 0; - } - - length = newLength; - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - - return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob); - } - - private object GetSegmentResult(string segmentPath, int index, int segmentLength, TranscodingJob transcodingJob) - { - var segmentEndingSeconds = (1 + index) * segmentLength; - var segmentEndingPositionTicks = TimeSpan.FromSeconds(segmentEndingSeconds).Ticks; - - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions - { - Path = segmentPath, - FileShare = FileShare.ReadWrite, - OnComplete = () => - { - if (transcodingJob != null) - { - transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); - } - - } - }); - } - - public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) - { - var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType); - - if (job == null || job.HasExited) - { - return null; - } - - var file = GetLastTranscodingFiles(playlist, segmentExtension, FileSystem, 1).FirstOrDefault(); - - if (file == null) - { - return null; - } - - return GetIndex(file.FullName); - } - - public int GetIndex(string segmentPath) - { - var indexString = Path.GetFileNameWithoutExtension(segmentPath).Split('-').LastOrDefault(); - - if (string.Equals(indexString, "init", StringComparison.OrdinalIgnoreCase)) - { - return -1; - } - var startNumber = int.Parse(Path.GetFileNameWithoutExtension(Path.GetDirectoryName(segmentPath)), NumberStyles.Integer, UsCulture); - - return startNumber + int.Parse(indexString, NumberStyles.Integer, UsCulture) - 1; - } - - private void DeleteLastTranscodedFiles(string playlistPath, int retryCount) - { - if (retryCount >= 5) - { - return; - } - } - - private static List<FileSystemMetadata> GetLastTranscodingFiles(string playlist, string segmentExtension, IFileSystem fileSystem, int count) - { - var folder = Path.GetDirectoryName(playlist); - - try - { - return fileSystem.GetFiles(folder) - .Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(fileSystem.GetLastWriteTimeUtc) - .Take(count) - .ToList(); - } - catch (DirectoryNotFoundException) - { - return new List<FileSystemMetadata>(); - } - } - - private string FindSegment(string playlist, string representationId, string segmentExtension, int requestedIndex) - { - var folder = Path.GetDirectoryName(playlist); - - if (requestedIndex == -1) - { - var path = Path.Combine(folder, "0", "stream" + representationId + "-" + "init" + segmentExtension); - return FileSystem.FileExists(path) ? path : null; - } - - try - { - foreach (var subfolder in FileSystem.GetDirectoryPaths(folder).ToList()) - { - var subfolderName = Path.GetFileNameWithoutExtension(subfolder); - int startNumber; - if (int.TryParse(subfolderName, NumberStyles.Any, UsCulture, out startNumber)) - { - var segmentIndex = requestedIndex - startNumber + 1; - var path = Path.Combine(folder, subfolderName, "stream" + representationId + "-" + segmentIndex.ToString("00000", CultureInfo.InvariantCulture) + segmentExtension); - if (FileSystem.FileExists(path)) - { - return path; - } - } - } - } - catch (DirectoryNotFoundException) - { - - } - - return null; - } - - protected override string GetAudioArguments(StreamState state) - { - var codec = GetAudioEncoder(state); - - if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) - { - return "-codec:a:0 copy"; - } - - var args = "-codec:a:0 " + codec; - - var channels = state.OutputAudioChannels; - - if (channels.HasValue) - { - args += " -ac " + channels.Value; - } - - var bitrate = state.OutputAudioBitrate; - - if (bitrate.HasValue) - { - args += " -ab " + bitrate.Value.ToString(UsCulture); - } - - args += " " + GetAudioFilterParam(state, true); - - return args; - } - - protected override string GetVideoArguments(StreamState state) - { - var codec = GetVideoEncoder(state); - - var args = "-codec:v:0 " + codec; - - if (state.EnableMpegtsM2TsMode) - { - args += " -mpegts_m2ts_mode 1"; - } - - // See if we can save come cpu cycles by avoiding encoding - if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) - { - return state.VideoStream != null && IsH264(state.VideoStream) ? - args + " -bsf:v h264_mp4toannexb" : - args; - } - - var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})", - state.SegmentLength.ToString(UsCulture)); - - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream; - - args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg; - - // Add resolution params, if specified - if (!hasGraphicalSubs) - { - args += GetOutputSizeParam(state, codec, false); - } - - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, codec); - } - - return args; - } - - protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) - { - // test url http://192.168.1.2:8096/videos/233e8905d559a8f230db9bffd2ac9d6d/master.mpd?mediasourceid=233e8905d559a8f230db9bffd2ac9d6d&videocodec=h264&audiocodec=aac&maxwidth=1280&videobitrate=500000&audiobitrate=128000&profile=baseline&level=3 - // Good info on i-frames http://blog.streamroot.io/encode-multi-bitrate-videos-mpeg-dash-mse-based-media-players/ - - var threads = GetNumberOfThreads(state, false); - - var inputModifier = GetInputModifier(state); - - var initSegmentName = "stream$RepresentationID$-init.m4s"; - var segmentName = "stream$RepresentationID$-$Number%05d$.m4s"; - - var args = string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -copyts {5} -f dash -init_seg_name \"{6}\" -media_seg_name \"{7}\" -use_template 0 -use_timeline 1 -min_seg_duration {8} -y \"{9}\"", - inputModifier, - GetInputArgument(state), - threads, - GetMapArgs(state), - GetVideoArguments(state), - GetAudioArguments(state), - initSegmentName, - segmentName, - (state.SegmentLength * 1000000).ToString(CultureInfo.InvariantCulture), - state.WaitForPath - ).Trim(); - - return args; - } - - protected override int GetStartNumber(StreamState state) - { - return GetStartNumber(state.VideoRequest); - } - - private int GetStartNumber(VideoStreamRequest request) - { - var segmentId = "0"; - - var segmentRequest = request as GetDashSegment; - if (segmentRequest != null) - { - segmentId = segmentRequest.SegmentId; - } - - if (string.Equals(segmentId, "init", StringComparison.OrdinalIgnoreCase)) - { - return -1; - } - - return int.Parse(segmentId, NumberStyles.Integer, UsCulture); - } - - /// <summary> - /// Gets the segment file extension. - /// </summary> - /// <param name="state">The state.</param> - /// <returns>System.String.</returns> - protected override string GetSegmentFileExtension(StreamState state) - { - return ".m4s"; - } - - protected override TranscodingJobType TranscodingJobType - { - get - { - return TranscodingJobType.Dash; - } - } - - private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken) - { - var segmentFilename = Path.GetFileName(segment); - - Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist); - - while (true) - { - // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = GetPlaylistFileStream(playlist)) - { - using (var reader = new StreamReader(fileStream)) - { - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - - if (line.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) - { - Logger.Debug("Finished waiting for {0} in {1}", segmentFilename, playlist); - return; - } - } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - } - } - } - } -} diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index c11265742..3d8957086 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -63,9 +63,9 @@ namespace MediaBrowser.Api.Playback.Hls /// <param name="request">The request.</param> /// <param name="isLive">if set to <c>true</c> [is live].</param> /// <returns>System.Object.</returns> - protected object ProcessRequest(StreamRequest request, bool isLive) + protected async Task<object> ProcessRequest(StreamRequest request, bool isLive) { - return ProcessRequestAsync(request, isLive).Result; + return await ProcessRequestAsync(request, isLive).ConfigureAwait(false); } /// <summary> @@ -83,11 +83,6 @@ namespace MediaBrowser.Api.Playback.Hls var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false); - if (isLive) - { - state.Request.StartTimeTicks = null; - } - TranscodingJob job = null; var playlist = state.OutputFilePath; @@ -137,13 +132,6 @@ namespace MediaBrowser.Api.Playback.Hls var appendBaselineStream = false; var baselineStreamBitrate = 64000; - var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; - if (hlsVideoRequest != null) - { - appendBaselineStream = hlsVideoRequest.AppendBaselineStream; - baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate; - } - var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate); job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); @@ -248,11 +236,7 @@ namespace MediaBrowser.Api.Playback.Hls protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) { - var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; - - var itsOffsetMs = hlsVideoRequest == null - ? 0 - : hlsVideoRequest.TimeStampOffsetMs; + var itsOffsetMs = 0; var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture)); @@ -286,26 +270,6 @@ namespace MediaBrowser.Api.Playback.Hls outputPath ).Trim(); - if (hlsVideoRequest != null) - { - if (hlsVideoRequest.AppendBaselineStream) - { - var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8"); - - var bitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? 64000; - - var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time {2} -start_number {3} -hls_list_size {4} -y \"{5}\"", - threads, - bitrate / 2, - state.SegmentLength.ToString(UsCulture), - startNumberParam, - state.HlsListSize.ToString(UsCulture), - lowBitratePath); - - args += " " + lowBitrateParams; - } - } - return args; } @@ -314,9 +278,16 @@ namespace MediaBrowser.Api.Playback.Hls return 0; } - protected override bool CanStreamCopyAudio(VideoStreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs) + protected bool IsLiveStream(StreamState state) { - return false; + var isLiveStream = (state.RunTimeTicks ?? 0) == 0; + + if (state.VideoRequest.ForceLiveStream) + { + return true; + } + + return isLiveStream; } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 3e46ee426..e029d4e99 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -475,7 +475,7 @@ namespace MediaBrowser.Api.Playback.Hls ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob); } } - }); + }).Result; } private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method) @@ -506,7 +506,7 @@ namespace MediaBrowser.Api.Playback.Hls builder.AppendLine("#EXTM3U"); - var isLiveStream = (state.RunTimeTicks ?? 0) == 0; + var isLiveStream = IsLiveStream(state); var queryStringIndex = Request.RawUrl.IndexOf('?'); var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex); @@ -525,10 +525,16 @@ namespace MediaBrowser.Api.Playback.Hls var subtitleGroup = subtitleStreams.Count > 0 && request is GetMasterHlsVideoPlaylist && - ((GetMasterHlsVideoPlaylist)request).SubtitleMethod == SubtitleDeliveryMethod.Hls ? + (state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ? "subs" : null; + // If we're burning in subtitles then don't add additional subs to the manifest + if (state.SubtitleStream != null && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode) + { + subtitleGroup = null; + } + if (!string.IsNullOrWhiteSpace(subtitleGroup)) { AddSubtitles(state, subtitleStreams, builder); @@ -572,13 +578,11 @@ namespace MediaBrowser.Api.Playback.Hls { const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\""; - var name = stream.Language; + var name = stream.DisplayTitle; var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index; var isForced = stream.IsForced; - if (string.IsNullOrWhiteSpace(name)) name = stream.Codec ?? "Unknown"; - var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}", state.Request.MediaSourceId, stream.Index.ToString(UsCulture), @@ -816,12 +820,12 @@ namespace MediaBrowser.Api.Playback.Hls // See if we can save come cpu cycles by avoiding encoding if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && IsH264(state.VideoStream)) + if (state.VideoStream != null && IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { args += " -bsf:v h264_mp4toannexb"; } - args += " -flags -global_header -sc_threshold 0"; + args += " -flags -global_header"; } else { @@ -846,7 +850,12 @@ namespace MediaBrowser.Api.Playback.Hls args += GetGraphicalSubtitleParam(state, codec); } - args += " -flags -global_header -sc_threshold 0"; + args += " -flags -global_header"; + } + + if (EnableCopyTs(state) && args.IndexOf("-copyts", StringComparison.OrdinalIgnoreCase) == -1) + { + args += " -copyts"; } return args; @@ -854,7 +863,8 @@ namespace MediaBrowser.Api.Playback.Hls private bool EnableCopyTs(StreamState state) { - return state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode; + //return state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode; + return true; } protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) @@ -876,24 +886,28 @@ namespace MediaBrowser.Api.Playback.Hls var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty; - //var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state); - - //return string.Format("{0} {11} {1}{10} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -segment_time {6} -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", - // inputModifier, - // GetInputArgument(state), - // threads, - // mapArgs, - // GetVideoArguments(state), - // GetAudioArguments(state), - // state.SegmentLength.ToString(UsCulture), - // startNumberParam, - // outputPath, - // outputTsArg, - // slowSeekParam, - // toTimeParam - // ).Trim(); - - return string.Format("{0}{11} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"", + var enableGenericSegmenter = false; + + if (enableGenericSegmenter) + { + var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state); + + return string.Format("{0} {10} {1} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + inputModifier, + GetInputArgument(state), + threads, + mapArgs, + GetVideoArguments(state), + GetAudioArguments(state), + state.SegmentLength.ToString(UsCulture), + startNumberParam, + outputPath, + outputTsArg, + toTimeParam + ).Trim(); + } + + return string.Format("{0}{11} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"", inputModifier, GetInputArgument(state), threads, @@ -928,11 +942,5 @@ namespace MediaBrowser.Api.Playback.Hls { return isOutputVideo ? ".ts" : ".ts"; } - - protected override bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream) - { - return false; - //return base.CanStreamCopyVideo(request, videoStream); - } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs index b44d7f660..27deaf25e 100644 --- a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs +++ b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs @@ -5,6 +5,7 @@ using ServiceStack; using System; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { @@ -32,25 +33,6 @@ namespace MediaBrowser.Api.Playback.Hls } /// <summary> - /// Class GetHlsVideoStream - /// </summary> - [Route("/Videos/{Id}/stream.m3u8", "GET")] - [Api(Description = "Gets a video stream using HTTP live streaming.")] - public class GetHlsVideoStreamLegacy : VideoStreamRequest - { - // TODO: Deprecate with new iOS app - - [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? BaselineStreamAudioBitRate { get; set; } - - [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool AppendBaselineStream { get; set; } - - [ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int TimeStampOffsetMs { get; set; } - } - - /// <summary> /// Class GetHlsVideoSegment /// </summary> [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] @@ -108,7 +90,7 @@ namespace MediaBrowser.Api.Playback.Hls _config = config; } - public object Get(GetHlsPlaylistLegacy request) + public Task<object> Get(GetHlsPlaylistLegacy request) { var file = request.PlaylistId + Path.GetExtension(Request.PathInfo); file = Path.Combine(_appPaths.TranscodingTempPath, file); @@ -126,7 +108,7 @@ namespace MediaBrowser.Api.Playback.Hls /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetHlsVideoSegmentLegacy request) + public Task<object> Get(GetHlsVideoSegmentLegacy request) { var file = request.SegmentId + Path.GetExtension(Request.PathInfo); file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file); @@ -150,10 +132,10 @@ namespace MediaBrowser.Api.Playback.Hls var file = request.SegmentId + Path.GetExtension(Request.PathInfo); file = Path.Combine(_appPaths.TranscodingTempPath, file); - return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite); + return ResultFactory.GetStaticFileResult(Request, file, FileShare.ReadWrite).Result; } - private object GetFileResult(string path, string playlistPath) + private Task<object> GetFileResult(string path, string playlistPath) { var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index f154a05cc..8a14948d2 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -27,16 +27,6 @@ namespace MediaBrowser.Api.Playback.Hls { } - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> - public object Get(GetHlsVideoStreamLegacy request) - { - return ProcessRequest(request, false); - } - public object Get(GetLiveHlsStream request) { return ProcessRequest(request, true); @@ -96,11 +86,14 @@ namespace MediaBrowser.Api.Playback.Hls // See if we can save come cpu cycles by avoiding encoding if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) { - return state.VideoStream != null && IsH264(state.VideoStream) ? - args + " -bsf:v h264_mp4toannexb" : - args; + // if h264_mp4toannexb is ever added, do not use it for live tv + if (state.VideoStream != null && IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + { + args += " -bsf:v h264_mp4toannexb"; + } + return args; } - + var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", state.SegmentLength.ToString(UsCulture)); diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 2bf61f90b..0b989784c 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -15,6 +15,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.MediaEncoding; namespace MediaBrowser.Api.Playback { @@ -66,14 +68,18 @@ namespace MediaBrowser.Api.Playback private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IUserManager _userManager; - public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager) + public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager, IMediaEncoder mediaEncoder, IUserManager userManager) { _mediaSourceManager = mediaSourceManager; _deviceManager = deviceManager; _libraryManager = libraryManager; _config = config; _networkManager = networkManager; + _mediaEncoder = mediaEncoder; + _userManager = userManager; } public object Get(GetBitrateTestBytes request) @@ -116,7 +122,7 @@ namespace MediaBrowser.Api.Playback SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex, - request.SubtitleStreamIndex, request.PlaySessionId); + request.SubtitleStreamIndex, request.PlaySessionId, request.UserId); } else { @@ -156,7 +162,7 @@ namespace MediaBrowser.Api.Playback { var mediaSourceId = request.MediaSourceId; - SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex); + SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.UserId); } return ToOptimizedResult(info); @@ -218,16 +224,17 @@ namespace MediaBrowser.Api.Playback long startTimeTicks, string mediaSourceId, int? audioStreamIndex, - int? subtitleStreamIndex) + int? subtitleStreamIndex, + string userId) { var item = _libraryManager.GetItemById(itemId); foreach (var mediaSource in result.MediaSources) { - SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, result.PlaySessionId); + SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, result.PlaySessionId, userId); } - SortMediaSources(result); + SortMediaSources(result, maxBitrate); } private void SetDeviceSpecificData(BaseItem item, @@ -239,9 +246,10 @@ namespace MediaBrowser.Api.Playback string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex, - string playSessionId) + string playSessionId, + string userId) { - var streamBuilder = new StreamBuilder(Logger); + var streamBuilder = new StreamBuilder(_mediaEncoder, Logger); var options = new VideoOptions { @@ -259,6 +267,8 @@ namespace MediaBrowser.Api.Playback options.SubtitleStreamIndex = subtitleStreamIndex; } + var user = _userManager.GetUserById(userId); + if (mediaSource.SupportsDirectPlay) { var supportsDirectStream = mediaSource.SupportsDirectStream; @@ -267,6 +277,14 @@ namespace MediaBrowser.Api.Playback mediaSource.SupportsDirectStream = true; options.MaxBitrate = maxBitrate; + if (item is Audio) + { + if (!user.Policy.EnableAudioPlaybackTranscoding) + { + options.ForceDirectPlay = true; + } + } + // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : @@ -290,6 +308,14 @@ namespace MediaBrowser.Api.Playback { options.MaxBitrate = GetMaxBitrate(maxBitrate); + if (item is Audio) + { + if (!user.Policy.EnableAudioPlaybackTranscoding) + { + options.ForceDirectStream = true; + } + } + // The MediaSource supports direct stream, now test to see if the client supports it var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ? streamBuilder.BuildAudioItem(options) : @@ -375,7 +401,7 @@ namespace MediaBrowser.Api.Playback } } - private void SortMediaSources(PlaybackInfoResponse result) + private void SortMediaSources(PlaybackInfoResponse result, int? maxBitrate) { var originalList = result.MediaSources.ToList(); @@ -409,6 +435,23 @@ namespace MediaBrowser.Api.Playback return 1; } + }).ThenBy(i => + { + if (maxBitrate.HasValue) + { + if (i.Bitrate.HasValue) + { + if (i.Bitrate.Value <= maxBitrate.Value) + { + return 0; + } + + return 2; + } + } + + return 1; + }).ThenBy(originalList.IndexOf) .ToList(); } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index 032a0719c..e828a53c9 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -9,6 +9,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using ServiceStack; using System.Collections.Generic; +using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.Api.Playback.Progressive @@ -40,7 +41,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetAudioStream request) + public Task<object> Get(GetAudioStream request) { return ProcessRequest(request, false); } @@ -50,7 +51,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Head(GetAudioStream request) + public Task<object> Head(GetAudioStream request) { return ProcessRequest(request, true); } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 3211f9e39..d75b8947a 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -13,6 +13,7 @@ using ServiceStack.Web; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Threading; using System.Threading.Tasks; using CommonIO; @@ -113,11 +114,11 @@ namespace MediaBrowser.Api.Playback.Progressive /// <param name="request">The request.</param> /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> /// <returns>Task.</returns> - protected object ProcessRequest(StreamRequest request, bool isHeadRequest) + protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest) { var cancellationTokenSource = new CancellationTokenSource(); - var state = GetState(request, cancellationTokenSource.Token).Result; + var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false); var responseHeaders = new Dictionary<string, string>(); @@ -128,7 +129,8 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { - return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result; + return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource) + .ConfigureAwait(false); } } @@ -138,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Progressive } var outputPath = state.OutputFilePath; - var outputPathExists = FileSystem.FileExists(outputPath); + var outputPathExists = FileSystem.FileExists(outputPath); var isTranscodeCached = outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive); @@ -151,13 +153,13 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, Path = state.MediaPath - }); + }).ConfigureAwait(false); } } @@ -168,13 +170,13 @@ namespace MediaBrowser.Api.Playback.Progressive try { - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, Path = outputPath - }); + }).ConfigureAwait(false); } finally { @@ -185,7 +187,8 @@ namespace MediaBrowser.Api.Playback.Progressive // Need to start ffmpeg try { - return GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).Result; + return await GetStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource) + .ConfigureAwait(false); } catch { @@ -229,7 +232,7 @@ namespace MediaBrowser.Api.Playback.Progressive if (trySupportSeek) { - foreach (var name in new[] {"Content-Range", "Accept-Ranges"}) + foreach (var name in new[] { "Content-Range", "Accept-Ranges" }) { var val = response.Headers[name]; if (!string.IsNullOrWhiteSpace(val)) @@ -242,12 +245,12 @@ namespace MediaBrowser.Api.Playback.Progressive { responseHeaders["Accept-Ranges"] = "none"; } - + if (response.ContentLength.HasValue) { responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture); } - + if (isHeadRequest) { using (response) @@ -324,7 +327,7 @@ namespace MediaBrowser.Api.Playback.Progressive { TranscodingJob job; - if (!FileSystem.FileExists(outputPath)) + if (!FileSystem.FileExists(outputPath)) { job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false); } @@ -334,17 +337,19 @@ namespace MediaBrowser.Api.Playback.Progressive state.Dispose(); } - var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job); + var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - result.Options["Content-Type"] = contentType; + outputHeaders["Content-Type"] = contentType; // Add the response headers to the result object foreach (var item in responseHeaders) { - result.Options[item.Key] = item.Value; + outputHeaders[item.Key] = item.Value; } - return result; + Func<Stream,Task> streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream, CancellationToken.None); + + return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders); } finally { diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 9f02c51cd..13d59240f 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -3,90 +3,12 @@ using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.Api.Playback.Progressive { - public class ProgressiveStreamWriter : IStreamWriter, IHasOptions - { - private string Path { get; set; } - private ILogger Logger { get; set; } - private readonly IFileSystem _fileSystem; - private readonly TranscodingJob _job; - - /// <summary> - /// The _options - /// </summary> - private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); - /// <summary> - /// Gets the options. - /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Options - { - get { return _options; } - } - - /// <summary> - /// Initializes a new instance of the <see cref="ProgressiveStreamWriter" /> class. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The file system.</param> - public ProgressiveStreamWriter(string path, ILogger logger, IFileSystem fileSystem, TranscodingJob job) - { - Path = path; - Logger = logger; - _fileSystem = fileSystem; - _job = job; - } - - /// <summary> - /// Writes to. - /// </summary> - /// <param name="responseStream">The response stream.</param> - public void WriteTo(Stream responseStream) - { - WriteToInternal(responseStream); - } - - /// <summary> - /// Writes to async. - /// </summary> - /// <param name="responseStream">The response stream.</param> - /// <returns>Task.</returns> - private void WriteToInternal(Stream responseStream) - { - try - { - var task = new ProgressiveFileCopier(_fileSystem, _job, Logger).StreamFile(Path, responseStream); - - Task.WaitAll(task); - } - catch (IOException) - { - // These error are always the same so don't dump the whole stack trace - Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed."); - - throw; - } - catch (Exception ex) - { - Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex); - - throw; - } - finally - { - if (_job != null) - { - ApiEntryPoint.Instance.OnTranscodeEndRequest(_job); - } - } - } - } - public class ProgressiveFileCopier { private readonly IFileSystem _fileSystem; @@ -105,22 +27,18 @@ namespace MediaBrowser.Api.Playback.Progressive _logger = logger; } - public async Task StreamFile(string path, Stream outputStream) + public async Task StreamFile(string path, Stream outputStream, CancellationToken cancellationToken) { var eofCount = 0; - long position = 0; - using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, false)) + using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { while (eofCount < 15) { - CopyToInternal(fs, outputStream, BufferSize); - - var fsPosition = fs.Position; - - var bytesRead = fsPosition - position; + var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false); - //Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path); + //var position = fs.Position; + //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); if (bytesRead == 0) { @@ -128,57 +46,36 @@ namespace MediaBrowser.Api.Playback.Progressive { eofCount++; } - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } else { eofCount = 0; } - - position = fsPosition; } } } - private void CopyToInternal(Stream source, Stream destination, int bufferSize) + private async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) { - var array = new byte[bufferSize]; - int count; - while ((count = source.Read(array, 0, array.Length)) != 0) - { - //if (_job != null) - //{ - // var didPause = false; - // var totalPauseTime = 0; + byte[] buffer = new byte[bufferSize]; + int bytesRead; + int totalBytesRead = 0; - // if (_job.IsUserPaused) - // { - // _logger.Debug("Pausing writing to network stream while user has paused playback."); - - // while (_job.IsUserPaused && totalPauseTime < 30000) - // { - // didPause = true; - // var pauseTime = 500; - // totalPauseTime += pauseTime; - // await Task.Delay(pauseTime).ConfigureAwait(false); - // } - // } - - // if (didPause) - // { - // _logger.Debug("Resuming writing to network stream due to user unpausing playback."); - // } - //} - - destination.Write(array, 0, count); + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); - _bytesWritten += count; + _bytesWritten += bytesRead; + totalBytesRead += bytesRead; if (_job != null) { _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); } } + + return totalBytesRead; } } } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index 7c68b1731..3fd67c51e 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.Serialization; using ServiceStack; using System; using System.IO; +using System.Threading.Tasks; using CommonIO; using MediaBrowser.Model.Dlna; @@ -76,7 +77,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetVideoStream request) + public Task<object> Get(GetVideoStream request) { return ProcessRequest(request, false); } @@ -86,7 +87,7 @@ namespace MediaBrowser.Api.Playback.Progressive /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Head(GetVideoStream request) + public Task<object> Head(GetVideoStream request) { return ProcessRequest(request, true); } @@ -137,12 +138,9 @@ namespace MediaBrowser.Api.Playback.Progressive args += " -mpegts_m2ts_mode 1"; } - var isOutputMkv = string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase); - if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && IsH264(state.VideoStream) && - (string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) || isOutputMkv)) + if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { args += " -bsf:v h264_mp4toannexb"; } diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index 1135a3a54..a8ca6aaa3 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -51,7 +51,9 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxAudioChannels { get; set; } - + + public int? TranscodingMaxAudioChannels { get; set; } + /// <summary> /// Gets or sets the audio sample rate. /// </summary> @@ -189,10 +191,11 @@ namespace MediaBrowser.Api.Playback [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool CopyTimestamps { get; set; } - - [ApiMember(Name = "Cabac", Description = "Enable if cabac encoding is required", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? Cabac { get; set; } - + + public bool ForceLiveStream { get; set; } + + public bool EnableSubtitlesInManifest { get; set; } + public VideoStreamRequest() { EnableAutoStreamCopy = true; diff --git a/MediaBrowser.Api/Playback/StreamState.cs b/MediaBrowser.Api/Playback/StreamState.cs index f1f6bb71f..da6be97b6 100644 --- a/MediaBrowser.Api/Playback/StreamState.cs +++ b/MediaBrowser.Api/Playback/StreamState.cs @@ -69,7 +69,29 @@ namespace MediaBrowser.Api.Playback public List<string> PlayableStreamFileNames { get; set; } - public int SegmentLength = 3; + public int SegmentLength + { + get + { + if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + var userAgent = UserAgent ?? string.Empty; + if (userAgent.IndexOf("AppleTV", StringComparison.OrdinalIgnoreCase) != -1) + { + return 10; + } + if (userAgent.IndexOf("cfnetwork", StringComparison.OrdinalIgnoreCase) != -1) + { + return 10; + } + + return 6; + } + + return 3; + } + } + public int HlsListSize { get @@ -84,9 +106,10 @@ namespace MediaBrowser.Api.Playback public long? InputFileSize { get; set; } public string OutputAudioSync = "1"; - public string OutputVideoSync = "vfr"; + public string OutputVideoSync = "-1"; public List<string> SupportedAudioCodecs { get; set; } + public string UserAgent { get; set; } public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger) { @@ -480,18 +503,5 @@ namespace MediaBrowser.Api.Playback return false; } } - - public bool? IsTargetCabac - { - get - { - if (Request.Static) - { - return VideoStream == null ? null : VideoStream.IsCabac; - } - - return true; - } - } } } diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 3dafd0eeb..604227a15 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -157,7 +157,7 @@ namespace MediaBrowser.Api Task.WaitAll(task); } - public object Get(GetPlaylistItems request) + public async Task<object> Get(GetPlaylistItems request) { var playlist = (Playlist)_libraryManager.GetItemById(request.Id); var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; @@ -178,7 +178,7 @@ namespace MediaBrowser.Api var dtoOptions = GetDtoOptions(request); - var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2), dtoOptions, user) + var dtos = (await _dtoService.GetBaseItemDtos(items.Select(i => i.Item2), dtoOptions, user).ConfigureAwait(false)) .ToArray(); var index = 0; diff --git a/MediaBrowser.Api/Reports/ReportsService.cs b/MediaBrowser.Api/Reports/ReportsService.cs index c3af09cd5..d0b6d6e78 100644 --- a/MediaBrowser.Api/Reports/ReportsService.cs +++ b/MediaBrowser.Api/Reports/ReportsService.cs @@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Reports /// <summary> Manager for library. </summary> private readonly ILibraryManager _libraryManager; ///< Manager for library - /// <summary> The localization. </summary> + /// <summary> The localization. </summary> private readonly ILocalizationManager _localization; ///< The localization @@ -58,10 +58,10 @@ namespace MediaBrowser.Api.Reports /// <summary> Gets the given request. </summary> /// <param name="request"> The request. </param> /// <returns> A Task<object> </returns> - public async Task<object> Get(GetActivityLogs request) + public object Get(GetActivityLogs request) { request.DisplayType = "Screen"; - ReportResult result = await GetReportActivities(request).ConfigureAwait(false); + ReportResult result = GetReportActivities(request); return ToOptimizedResult(result); } @@ -104,7 +104,8 @@ namespace MediaBrowser.Api.Reports return null; request.DisplayType = "Screen"; - var reportResult = await GetReportResult(request); + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; + var reportResult = await GetReportResult(request, user); return ToOptimizedResult(reportResult); } @@ -117,7 +118,8 @@ namespace MediaBrowser.Api.Reports if (string.IsNullOrEmpty(request.IncludeItemTypes)) return null; request.DisplayType = "Screen"; - var reportResult = await GetReportStatistic(request); + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; + var reportResult = await GetReportStatistic(request, user); return ToOptimizedResult(reportResult); } @@ -150,6 +152,7 @@ namespace MediaBrowser.Api.Reports headers["Content-Disposition"] = string.Format("attachment; filename=\"{0}\"", filename); headers["Content-Encoding"] = "UTF-8"; + var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; ReportResult result = null; switch (reportViewType) { @@ -157,12 +160,12 @@ namespace MediaBrowser.Api.Reports case ReportViewType.ReportData: ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); ReportBuilder dataBuilder = new ReportBuilder(_libraryManager); - QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false); result = dataBuilder.GetResult(queryResult.Items, request); result.TotalRecordCount = queryResult.TotalRecordCount; break; case ReportViewType.ReportActivities: - result = await GetReportActivities(request).ConfigureAwait(false); + result = GetReportActivities(request); break; } @@ -177,23 +180,15 @@ namespace MediaBrowser.Api.Reports break; } - object ro = ResultFactory.GetResult(returnResult, contentType, headers); - return ro; + return ResultFactory.GetResult(returnResult, contentType, headers); } #endregion - #region [Private Methods] - - /// <summary> Gets items query. </summary> - /// <param name="request"> The request. </param> - /// <param name="user"> The user. </param> - /// <returns> The items query. </returns> private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user) { - var query = new InternalItemsQuery + var query = new InternalItemsQuery(user) { - User = user, IsPlayed = request.IsPlayed, MediaTypes = request.GetMediaTypes(), IncludeItemTypes = request.GetIncludeItemTypes(), @@ -213,7 +208,6 @@ namespace MediaBrowser.Api.Reports NameStartsWith = request.NameStartsWith, NameStartsWithOrGreater = request.NameStartsWithOrGreater, HasImdbId = request.HasImdbId, - IsYearMismatched = request.IsYearMismatched, IsPlaceHolder = request.IsPlaceHolder, IsLocked = request.IsLocked, IsInBoxSet = request.IsInBoxSet, @@ -232,6 +226,7 @@ namespace MediaBrowser.Api.Reports Tags = request.GetTags(), OfficialRatings = request.GetOfficialRatings(), Genres = request.GetGenres(), + GenreIds = request.GetGenreIds(), Studios = request.GetStudios(), StudioIds = request.GetStudioIds(), Person = request.Person, @@ -246,9 +241,11 @@ namespace MediaBrowser.Api.Reports MaxPlayers = request.MaxPlayers, MinCommunityRating = request.MinCommunityRating, MinCriticRating = request.MinCriticRating, + ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId), ParentIndexNumber = request.ParentIndexNumber, AiredDuringSeason = request.AiredDuringSeason, - AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater + AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater, + EnableTotalRecordCount = request.EnableTotalRecordCount }; if (!string.IsNullOrWhiteSpace(request.Ids)) @@ -326,15 +323,15 @@ namespace MediaBrowser.Api.Reports } // Min official rating - if (!string.IsNullOrEmpty(request.MinOfficialRating)) + if (!string.IsNullOrWhiteSpace(request.MinOfficialRating)) { query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating); } // Max official rating - if (!string.IsNullOrEmpty(request.MaxOfficialRating)) + if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating)) { - query.MaxParentalRating = _localization.GetRatingLevel(request.MinOfficialRating); + query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating); } // Artists @@ -358,98 +355,111 @@ namespace MediaBrowser.Api.Reports query.AlbumNames = request.Albums.Split('|'); } - if (request.HasQueryLimit == false) - { - query.StartIndex = null; - query.Limit = null; - } - return query; } - /// <summary> Gets query result. </summary> - /// <param name="request"> The request. </param> - /// <returns> The query result. </returns> - private async Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request) + private async Task<QueryResult<BaseItem>> GetQueryResult(BaseReportRequest request, User user) { - // Placeholder in case needed later + // all report queries currently need this because it's not being specified request.Recursive = true; - var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; - request.Fields = "MediaSources,DateCreated,Settings,Studios,SyncInfo,ItemCounts"; - - var parentItem = string.IsNullOrEmpty(request.ParentId) ? - (user == null ? _libraryManager.RootFolder : user.RootFolder) : - _libraryManager.GetItemById(request.ParentId); var item = string.IsNullOrEmpty(request.ParentId) ? user == null ? _libraryManager.RootFolder : user.RootFolder : - parentItem; + _libraryManager.GetItemById(request.ParentId); - IEnumerable<BaseItem> items; + 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; + } - if (request.Recursive) + // Default list type = children + + var folder = item as Folder; + if (folder == null) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); - return result; + folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); } - else + + if (!string.IsNullOrEmpty(request.Ids)) { - if (user == null) + request.Recursive = true; + var query = GetItemsQuery(request, user); + var result = await folder.GetItems(query).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(request.SortBy)) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); - return result; + var ids = query.ItemIds.ToList(); + + // Try to preserve order + result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); } - var userRoot = item as UserRootFolder; + return result; + } - if (userRoot == null) - { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + if (request.Recursive) + { + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); + } - return result; - } + if (user == null) + { + return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); + } + + var userRoot = item as UserRootFolder; - items = ((Folder)item).GetChildren(user, true); + if (userRoot == null) + { + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } - return new QueryResult<BaseItem> { Items = items.ToArray() }; + IEnumerable<BaseItem> items = folder.GetChildren(user, true); + var itemsArray = items.ToArray(); + + return new QueryResult<BaseItem> + { + Items = itemsArray, + TotalRecordCount = itemsArray.Length + }; } + #region [Private Methods] + /// <summary> Gets report activities. </summary> /// <param name="request"> The request. </param> /// <returns> The report activities. </returns> - private Task<ReportResult> GetReportActivities(IReportsDownload request) + private ReportResult GetReportActivities(IReportsDownload request) { - return Task<ReportResult>.Run(() => - { - DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? - (DateTime?)null : - DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - - QueryResult<ActivityLogEntry> queryResult; - if (request.HasQueryLimit) - queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); - else - queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null); - //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); - - ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager); - var result = builder.GetResult(queryResult, request); - result.TotalRecordCount = queryResult.TotalRecordCount; - return result; + DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? + (DateTime?)null : + DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - }); + QueryResult<ActivityLogEntry> queryResult; + if (request.HasQueryLimit) + queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); + else + queryResult = _repo.GetActivityLogEntries(minDate, request.StartIndex, null); + //var queryResult = _activityManager.GetActivityLogEntries(minDate, request.StartIndex, request.Limit); + ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager); + var result = builder.GetResult(queryResult, request); + result.TotalRecordCount = queryResult.TotalRecordCount; + return result; } /// <summary> Gets report result. </summary> /// <param name="request"> The request. </param> /// <returns> The report result. </returns> - private async Task<ReportResult> GetReportResult(GetItemReport request) + private async Task<ReportResult> GetReportResult(GetItemReport request, User user) { ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); - QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false); ReportResult reportResult = reportBuilder.GetResult(queryResult.Items, request); reportResult.TotalRecordCount = queryResult.TotalRecordCount; @@ -459,10 +469,10 @@ namespace MediaBrowser.Api.Reports /// <summary> Gets report statistic. </summary> /// <param name="request"> The request. </param> /// <returns> The report statistic. </returns> - private async Task<ReportStatResult> GetReportStatistic(GetReportStatistics request) + private async Task<ReportStatResult> GetReportStatistic(GetReportStatistics request, User user) { ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); - QueryResult<BaseItem> queryResult = await GetQueryResult(request).ConfigureAwait(false); + QueryResult<BaseItem> queryResult = await GetQueryResult(request, user).ConfigureAwait(false); ReportStatBuilder reportBuilder = new ReportStatBuilder(_libraryManager); ReportStatResult reportResult = reportBuilder.GetResult(queryResult.Items, ReportHelper.GetRowType(request.IncludeItemTypes), request.TopItems ?? 5); diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index 277bba1dd..a1e47bd8f 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -9,6 +9,8 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Api { @@ -23,6 +25,8 @@ namespace MediaBrowser.Api /// <value>The id.</value> [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } + + public string ExcludeArtistIds { get; set; } } public class BaseGetSimilarItems : IReturn<ItemsResult>, IHasItemFields @@ -54,7 +58,7 @@ namespace MediaBrowser.Api /// </summary> public static class SimilarItemsHelper { - internal static ItemsResult GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, ILogger logger, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore) + internal static async Task<QueryResult<BaseItemDto>> GetSimilarItemsResult(DtoOptions dtoOptions, IUserManager userManager, IItemRepository itemRepository, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, ILogger logger, BaseGetSimilarItemsFromItem request, Type[] includeTypes, Func<BaseItem, List<PersonInfo>, List<PersonInfo>, BaseItem, int> getSimilarityScore) { var user = !string.IsNullOrWhiteSpace(request.UserId) ? userManager.GetUserById(request.UserId) : null; @@ -68,6 +72,12 @@ namespace MediaBrowser.Api Recursive = true }; + // ExcludeArtistIds + if (!string.IsNullOrEmpty(request.ExcludeArtistIds)) + { + query.ExcludeArtistIds = request.ExcludeArtistIds.Split('|'); + } + var inputItems = libraryManager.GetItemList(query); var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore) @@ -80,14 +90,14 @@ namespace MediaBrowser.Api returnItems = returnItems.Take(request.Limit.Value); } - var result = new ItemsResult + var dtos = await dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ConfigureAwait(false); + + return new QueryResult<BaseItemDto> { - Items = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user).ToArray(), + Items = dtos.ToArray(), TotalRecordCount = items.Count }; - - return result; } /// <summary> @@ -116,24 +126,12 @@ namespace MediaBrowser.Api private static IEnumerable<string> GetTags(BaseItem item) { - var hasTags = item as IHasTags; - if (hasTags != null) - { - return hasTags.Tags; - } - - return new List<string>(); + return item.Tags; } private static IEnumerable<string> GetKeywords(BaseItem item) { - var hasTags = item as IHasKeywords; - if (hasTags != null) - { - return hasTags.Keywords; - } - - return new List<string>(); + return item.Keywords; } /// <summary> diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 14bd6b61f..1bebd42eb 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -11,6 +11,7 @@ using ServiceStack; using System; using System.Linq; using System.Threading.Tasks; +using MediaBrowser.Controller.MediaEncoding; namespace MediaBrowser.Api { @@ -52,34 +53,33 @@ namespace MediaBrowser.Api private readonly IUserManager _userManager; private readonly IConnectManager _connectManager; private readonly ILiveTvManager _liveTvManager; + private readonly IMediaEncoder _mediaEncoder; - public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager) + public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager, IMediaEncoder mediaEncoder) { _config = config; _appHost = appHost; _userManager = userManager; _connectManager = connectManager; _liveTvManager = liveTvManager; + _mediaEncoder = mediaEncoder; } public void Post(ReportStartupWizardComplete request) { _config.Configuration.IsStartupWizardCompleted = true; - _config.Configuration.EnableLocalizedGuids = true; - _config.Configuration.EnableCustomPathSubFolders = true; - _config.Configuration.EnableDateLastRefresh = true; - _config.Configuration.EnableStandaloneMusicKeys = true; - _config.Configuration.EnableCaseSensitiveItemIds = true; + SetWizardFinishValues(_config.Configuration); _config.SaveConfiguration(); } - public object Get(GetStartupInfo request) + public async Task<object> Get(GetStartupInfo request) { - var info = _appHost.GetSystemInfo(); + var info = await _appHost.GetSystemInfo().ConfigureAwait(false); return new StartupInfo { - SupportsRunningAsService = info.SupportsRunningAsService + SupportsRunningAsService = info.SupportsRunningAsService, + HasMediaEncoder = !string.IsNullOrWhiteSpace(_mediaEncoder.EncoderPath) }; } @@ -111,6 +111,15 @@ namespace MediaBrowser.Api return result; } + private void SetWizardFinishValues(ServerConfiguration config) + { + config.EnableLocalizedGuids = true; + config.EnableStandaloneMusicKeys = true; + config.EnableCaseSensitiveItemIds = true; + //config.EnableFolderView = true; + config.SchemaVersion = 108; + } + public void Post(UpdateStartupConfiguration request) { _config.Configuration.UICulture = request.UICulture; @@ -225,6 +234,7 @@ namespace MediaBrowser.Api public class StartupInfo { public bool SupportsRunningAsService { get; set; } + public bool HasMediaEncoder { get; set; } } public class StartupUser diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index c3f31e75a..fe13e8b21 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -98,6 +98,10 @@ namespace MediaBrowser.Api.Subtitles [ApiMember(Name = "EndPositionTicks", Description = "EndPositionTicks", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public long? EndPositionTicks { get; set; } + + [ApiMember(Name = "CopyTimestamps", Description = "CopyTimestamps", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool CopyTimestamps { get; set; } + public bool AddVttTimeMap { get; set; } } [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")] @@ -159,6 +163,7 @@ namespace MediaBrowser.Api.Subtitles builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture)); builder.AppendLine("#EXT-X-VERSION:3"); builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); long positionTicks = 0; var segmentLengthTicks = TimeSpan.FromSeconds(request.SegmentLength).Ticks; @@ -170,11 +175,11 @@ namespace MediaBrowser.Api.Subtitles var remaining = runtime - positionTicks; var lengthTicks = Math.Min(remaining, segmentLengthTicks); - builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); - var url = string.Format("stream.vtt?StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", + var url = string.Format("stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", positionTicks.ToString(CultureInfo.InvariantCulture), endPositionTicks.ToString(CultureInfo.InvariantCulture), accessToken); @@ -189,7 +194,7 @@ namespace MediaBrowser.Api.Subtitles return ResultFactory.GetResult(builder.ToString(), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>()); } - public object Get(GetSubtitle request) + public async Task<object> Get(GetSubtitle request) { if (string.Equals(request.Format, "js", StringComparison.OrdinalIgnoreCase)) { @@ -205,23 +210,35 @@ namespace MediaBrowser.Api.Subtitles var subtitleStream = mediaSource.MediaStreams .First(i => i.Type == MediaStreamType.Subtitle && i.Index == request.Index); - return ToStaticFileResult(subtitleStream.Path); + return await ResultFactory.GetStaticFileResult(Request, subtitleStream.Path).ConfigureAwait(false); } - var stream = GetSubtitles(request).Result; + using (var stream = await GetSubtitles(request).ConfigureAwait(false)) + { + using (var reader = new StreamReader(stream)) + { + var text = reader.ReadToEnd(); + + if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap) + { + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); + } - return ResultFactory.GetResult(stream, MimeTypes.GetMimeType("file." + request.Format)); + return ResultFactory.GetResult(text, MimeTypes.GetMimeType("file." + request.Format)); + } + } } - private async Task<Stream> GetSubtitles(GetSubtitle request) + private Task<Stream> GetSubtitles(GetSubtitle request) { - return await _subtitleEncoder.GetSubtitles(request.Id, + return _subtitleEncoder.GetSubtitles(request.Id, request.MediaSourceId, request.Index, request.Format, request.StartPositionTicks, request.EndPositionTicks, - CancellationToken.None).ConfigureAwait(false); + request.CopyTimestamps, + CancellationToken.None); } public object Get(SearchRemoteSubtitles request) @@ -247,9 +264,9 @@ namespace MediaBrowser.Api.Subtitles return ToOptimizedResult(result); } - public object Get(GetRemoteSubtitles request) + public async Task<object> Get(GetRemoteSubtitles request) { - var result = _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).Result; + var result = await _subtitleManager.GetRemoteSubtitles(request.Id, CancellationToken.None).ConfigureAwait(false); return ResultFactory.GetResult(result.Stream, MimeTypes.GetMimeType("file." + result.Format)); } diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index 593c3a108..a15ce216f 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -227,7 +227,7 @@ namespace MediaBrowser.Api.Sync Task.WaitAll(task); } - public object Get(GetSyncJobItemFile request) + public async Task<object> Get(GetSyncJobItemFile request) { var jobItem = _syncManager.GetJobItem(request.Id); @@ -241,10 +241,9 @@ namespace MediaBrowser.Api.Sync throw new ArgumentException("The job item is not yet ready for transfer."); } - var task = _syncManager.ReportSyncJobItemTransferBeginning(request.Id); - Task.WaitAll(task); + await _syncManager.ReportSyncJobItemTransferBeginning(request.Id).ConfigureAwait(false); - return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { Path = jobItem.OutputPath, OnError = () => @@ -252,10 +251,11 @@ namespace MediaBrowser.Api.Sync var failedTask = _syncManager.ReportSyncJobItemTransferFailed(request.Id); Task.WaitAll(failedTask); } - }); + + }).ConfigureAwait(false); } - public object Get(GetSyncDialogOptions request) + public async Task<object> Get(GetSyncDialogOptions request) { var result = new SyncDialogOptions(); @@ -298,8 +298,7 @@ namespace MediaBrowser.Api.Sync .Select(_libraryManager.GetItemById) .Where(i => i != null); - var dtos = _dtoService.GetBaseItemDtos(items, dtoOptions, authenticatedUser) - .ToList(); + var dtos = (await _dtoService.GetBaseItemDtos(items, dtoOptions, authenticatedUser).ConfigureAwait(false)); result.Options = SyncHelper.GetSyncOptions(dtos); } @@ -343,7 +342,7 @@ namespace MediaBrowser.Api.Sync Task.WaitAll(task); } - public object Get(GetSyncJobItemAdditionalFile request) + public Task<object> Get(GetSyncJobItemAdditionalFile request) { var jobItem = _syncManager.GetJobItem(request.Id); @@ -359,7 +358,7 @@ namespace MediaBrowser.Api.Sync throw new ArgumentException("Sync job additional file not found."); } - return ToStaticFileResult(file.Path); + return ResultFactory.GetStaticFileResult(Request, file.Path); } public void Post(EnableSyncJobItem request) diff --git a/MediaBrowser.Api/System/SystemInfoWebSocketListener.cs b/MediaBrowser.Api/System/SystemInfoWebSocketListener.cs index 9ab7770ed..a53bfac27 100644 --- a/MediaBrowser.Api/System/SystemInfoWebSocketListener.cs +++ b/MediaBrowser.Api/System/SystemInfoWebSocketListener.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Api.System /// <returns>Task{SystemInfo}.</returns> protected override Task<SystemInfo> GetDataToSend(WebSocketListenerState state) { - return Task.FromResult(_appHost.GetSystemInfo()); + return _appHost.GetSystemInfo(); } } } diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs index b4b41c844..c2318dccb 100644 --- a/MediaBrowser.Api/System/SystemService.cs +++ b/MediaBrowser.Api/System/SystemService.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Api.System /// Class GetSystemInfo /// </summary> [Route("/System/Info", "GET", Summary = "Gets information about the server")] - [Authenticated(EscapeParentalControl = true)] + [Authenticated(EscapeParentalControl = true, AllowBeforeStartupWizard = true)] public class GetSystemInfo : IReturn<SystemInfo> { @@ -120,7 +120,7 @@ namespace MediaBrowser.Api.System try { - files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) + files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .Where(i => string.Equals(i.Extension, ".txt", StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -144,9 +144,9 @@ namespace MediaBrowser.Api.System return ToOptimizedResult(result); } - public object Get(GetLogFile request) + public Task<object> Get(GetLogFile request) { - var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) + var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite); @@ -157,16 +157,16 @@ namespace MediaBrowser.Api.System /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetSystemInfo request) + public async Task<object> Get(GetSystemInfo request) { - var result = _appHost.GetSystemInfo(); + var result = await _appHost.GetSystemInfo().ConfigureAwait(false); return ToOptimizedResult(result); } - public object Get(GetPublicSystemInfo request) + public async Task<object> Get(GetPublicSystemInfo request) { - var result = _appHost.GetSystemInfo(); + var result = await _appHost.GetSystemInfo().ConfigureAwait(false); var publicInfo = new PublicSystemInfo { diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 5b5b0a902..3f248ea8f 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -12,6 +12,8 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Api { @@ -123,7 +125,7 @@ namespace MediaBrowser.Api } [Route("/Shows/{Id}/Episodes", "GET", Summary = "Gets episodes for a tv season")] - public class GetEpisodes : IReturn<ItemsResult>, IHasItemFields + public class GetEpisodes : IReturn<ItemsResult>, IHasItemFields, IHasDtoOptions { /// <summary> /// Gets or sets the user id. @@ -173,10 +175,19 @@ namespace MediaBrowser.Api /// <value>The limit.</value> [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Limit { get; set; } + + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } } [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")] - public class GetSeasons : IReturn<ItemsResult>, IHasItemFields + public class GetSeasons : IReturn<ItemsResult>, IHasItemFields, IHasDtoOptions { /// <summary> /// Gets or sets the user id. @@ -206,6 +217,15 @@ namespace MediaBrowser.Api [ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string AdjacentTo { get; set; } + + [ApiMember(Name = "EnableImages", Description = "Optional, include image information in output", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableImages { get; set; } + + [ApiMember(Name = "ImageTypeLimit", Description = "Optional, the max number of images to return, per image type", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int? ImageTypeLimit { get; set; } + + [ApiMember(Name = "EnableImageTypes", Description = "Optional. The image types to include in the output.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string EnableImageTypes { get; set; } } /// <summary> @@ -253,29 +273,51 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetSimilarShows request) + public async Task<object> Get(GetSimilarShows request) { + var result = await GetSimilarItemsResult(request).ConfigureAwait(false); + + return ToOptimizedSerializedResultUsingCache(result); + } + + private async Task<QueryResult<BaseItemDto>> GetSimilarItemsResult(BaseGetSimilarItemsFromItem request) + { + 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); + + var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) + { + Limit = request.Limit, + IncludeItemTypes = new[] + { + typeof(Series).Name + }, + SimilarTo = item + + }).ToList(); + var dtoOptions = GetDtoOptions(request); - var result = SimilarItemsHelper.GetSimilarItemsResult(dtoOptions, _userManager, - _itemRepo, - _libraryManager, - _userDataManager, - _dtoService, - Logger, - request, new[] { typeof(Series) }, - SimilarItemsHelper.GetSimiliarityScore); + var result = new QueryResult<BaseItemDto> + { + Items = (await _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user).ConfigureAwait(false)).ToArray(), - return ToOptimizedSerializedResultUsingCache(result); + TotalRecordCount = itemsResult.Count + }; + + return result; } - public object Get(GetUpcomingEpisodes request) + public async Task<object> Get(GetUpcomingEpisodes request) { var user = _userManager.GetUserById(request.UserId); - var minPremiereDate = DateTime.Now.Date.ToUniversalTime(); + var minPremiereDate = DateTime.Now.Date.ToUniversalTime().AddDays(-1); - var parentIds = string.IsNullOrWhiteSpace(request.ParentId) ? new string[] { } : new[] { request.ParentId }; + var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) { @@ -284,13 +326,15 @@ namespace MediaBrowser.Api SortOrder = SortOrder.Ascending, MinPremiereDate = minPremiereDate, StartIndex = request.StartIndex, - Limit = request.Limit + Limit = request.Limit, + ParentId = parentIdGuid, + Recursive = true - }, parentIds).ToList(); + }).ToList(); var options = GetDtoOptions(request); - var returnItems = _dtoService.GetBaseItemDtos(itemsResult, options, user).ToArray(); + var returnItems = (await _dtoService.GetBaseItemDtos(itemsResult, options, user).ConfigureAwait(false)).ToArray(); var result = new ItemsResult { @@ -306,7 +350,7 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public object Get(GetNextUpEpisodes request) + public async Task<object> Get(GetNextUpEpisodes request) { var result = _tvSeriesManager.GetNextUp(new NextUpQuery { @@ -321,7 +365,7 @@ namespace MediaBrowser.Api var options = GetDtoOptions(request); - var returnItems = _dtoService.GetBaseItemDtos(result.Items, options, user).ToArray(); + var returnItems = (await _dtoService.GetBaseItemDtos(result.Items, options, user).ConfigureAwait(false)).ToArray(); return ToOptimizedSerializedResultUsingCache(new ItemsResult { @@ -354,7 +398,7 @@ namespace MediaBrowser.Api return items; } - public object Get(GetSeasons request) + public async Task<object> Get(GetSeasons request) { var user = _userManager.GetUserById(request.UserId); @@ -385,7 +429,7 @@ namespace MediaBrowser.Api var dtoOptions = GetDtoOptions(request); - var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user) + var returnItems = (await _dtoService.GetBaseItemDtos(seasons, dtoOptions, user).ConfigureAwait(false)) .ToArray(); return new ItemsResult @@ -397,21 +441,10 @@ namespace MediaBrowser.Api private IEnumerable<Season> FilterVirtualSeasons(GetSeasons request, IEnumerable<Season> items) { - if (request.IsMissing.HasValue && request.IsVirtualUnaired.HasValue) - { - var isMissing = request.IsMissing.Value; - var isVirtualUnaired = request.IsVirtualUnaired.Value; - - if (!isMissing && !isVirtualUnaired) - { - return items.Where(i => !i.IsMissingOrVirtualUnaired); - } - } - if (request.IsMissing.HasValue) { var val = request.IsMissing.Value; - items = items.Where(i => i.IsMissingSeason == val); + items = items.Where(i => (i.IsMissingSeason) == val); } if (request.IsVirtualUnaired.HasValue) @@ -423,7 +456,7 @@ namespace MediaBrowser.Api return items; } - public object Get(GetEpisodes request) + public async Task<object> Get(GetEpisodes request) { var user = _userManager.GetUserById(request.UserId); @@ -449,7 +482,16 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("No series exists with Id " + request.Id); } - episodes = series.GetEpisodes(user, request.Season.Value); + var season = series.GetSeasons(user).FirstOrDefault(i => i.IndexNumber == request.Season.Value); + + if (season == null) + { + episodes = new List<Episode>(); + } + else + { + episodes = series.GetEpisodes(user, season); + } } else { @@ -490,14 +532,13 @@ namespace MediaBrowser.Api returnItems = UserViewBuilder.FilterForAdjacency(returnItems, request.AdjacentTo); } - var returnList = _libraryManager.ReplaceVideosWithPrimaryVersions(returnItems) - .ToList(); + var returnList = returnItems.ToList(); var pagedItems = ApplyPaging(returnList, request.StartIndex, request.Limit); var dtoOptions = GetDtoOptions(request); - var dtos = _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user) + var dtos = (await _dtoService.GetBaseItemDtos(pagedItems, dtoOptions, user).ConfigureAwait(false)) .ToArray(); return new ItemsResult diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index cde5eade5..df73ef720 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Dto; +using System; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -8,6 +9,8 @@ using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary { @@ -100,7 +103,12 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetArtists request) { - var result = GetResult(request); + if (string.IsNullOrWhiteSpace(request.IncludeItemTypes)) + { + //request.IncludeItemTypes = "Audio,MusicVideo"; + } + + var result = GetResultSlim(request); return ToOptimizedResult(result); } @@ -112,11 +120,26 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetAlbumArtists request) { - var result = GetResult(request); + if (string.IsNullOrWhiteSpace(request.IncludeItemTypes)) + { + //request.IncludeItemTypes = "Audio,MusicVideo"; + } + + var result = GetResultSlim(request); return ToOptimizedResult(result); } + protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) + { + if (request is GetAlbumArtists) + { + return LibraryManager.GetAlbumArtists(query); + } + + return LibraryManager.GetArtists(query); + } + /// <summary> /// Gets all items. /// </summary> @@ -125,16 +148,7 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items) { - if (request is GetAlbumArtists) - { - return LibraryManager.GetAlbumArtists(items - .Where(i => !i.IsFolder) - .OfType<IHasAlbumArtist>()); - } - - return LibraryManager.GetArtists(items - .Where(i => !i.IsFolder) - .OfType<IHasArtist>()); + throw new NotImplementedException(); } } } diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 6ae2b0832..9465d1fdc 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -8,6 +8,7 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Api.UserLibrary { @@ -83,6 +84,137 @@ namespace MediaBrowser.Api.UserLibrary return null; } + protected ItemsResult GetResultSlim(GetItemsByName request) + { + var dtoOptions = GetDtoOptions(request); + + User user = null; + BaseItem parentItem; + + if (!string.IsNullOrWhiteSpace(request.UserId)) + { + user = UserManager.GetUserById(request.UserId); + parentItem = string.IsNullOrEmpty(request.ParentId) ? user.RootFolder : LibraryManager.GetItemById(request.ParentId); + } + else + { + parentItem = string.IsNullOrEmpty(request.ParentId) ? LibraryManager.RootFolder : LibraryManager.GetItemById(request.ParentId); + } + + var excludeItemTypes = request.GetExcludeItemTypes(); + var includeItemTypes = request.GetIncludeItemTypes(); + var mediaTypes = request.GetMediaTypes(); + + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, + StartIndex = request.StartIndex, + Limit = request.Limit, + IsFavorite = request.IsFavorite, + NameLessThan = request.NameLessThan, + NameStartsWith = request.NameStartsWith, + NameStartsWithOrGreater = request.NameStartsWithOrGreater, + AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater, + Tags = request.GetTags(), + OfficialRatings = request.GetOfficialRatings(), + Genres = request.GetGenres(), + GenreIds = request.GetGenreIds(), + Studios = request.GetStudios(), + StudioIds = request.GetStudioIds(), + Person = request.Person, + PersonIds = request.GetPersonIds(), + PersonTypes = request.GetPersonTypes(), + Years = request.GetYears(), + MinCommunityRating = request.MinCommunityRating + }; + + if (!string.IsNullOrWhiteSpace(request.ParentId)) + { + if (parentItem is Folder) + { + query.AncestorIds = new[] { request.ParentId }; + } + else + { + query.ItemIds = new[] { request.ParentId }; + } + } + + foreach (var filter in request.GetFilters()) + { + switch (filter) + { + case ItemFilter.Dislikes: + query.IsLiked = false; + break; + case ItemFilter.IsFavorite: + query.IsFavorite = true; + break; + case ItemFilter.IsFavoriteOrLikes: + query.IsFavoriteOrLiked = true; + break; + case ItemFilter.IsFolder: + query.IsFolder = true; + break; + case ItemFilter.IsNotFolder: + query.IsFolder = false; + break; + case ItemFilter.IsPlayed: + query.IsPlayed = true; + break; + case ItemFilter.IsRecentlyAdded: + break; + case ItemFilter.IsResumable: + query.IsResumable = true; + break; + case ItemFilter.IsUnplayed: + query.IsPlayed = false; + break; + case ItemFilter.Likes: + query.IsLiked = true; + break; + } + } + + var result = GetItems(request, query); + + var dtos = result.Items.Select(i => + { + var dto = DtoService.GetItemByNameDto(i.Item1, dtoOptions, null, user); + + if (!string.IsNullOrWhiteSpace(request.IncludeItemTypes)) + { + SetItemCounts(dto, i.Item2); + } + return dto; + }); + + return new ItemsResult + { + Items = dtos.ToArray(), + TotalRecordCount = result.TotalRecordCount + }; + } + + protected virtual QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return new QueryResult<Tuple<BaseItem, ItemCounts>>(); + } + + private void SetItemCounts(BaseItemDto dto, ItemCounts counts) + { + dto.ChildCount = counts.ItemCount; + dto.SeriesCount = counts.SeriesCount; + dto.EpisodeCount = counts.EpisodeCount; + dto.MovieCount = counts.MovieCount; + dto.TrailerCount = counts.TrailerCount; + dto.AlbumCount = counts.AlbumCount; + dto.SongCount = counts.SongCount; + dto.GameCount = counts.GameCount; + } + /// <summary> /// Gets the specified request. /// </summary> @@ -121,6 +253,13 @@ namespace MediaBrowser.Api.UserLibrary var includeItemTypes = request.GetIncludeItemTypes(); var mediaTypes = request.GetMediaTypes(); + var query = new InternalItemsQuery(user) + { + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes + }; + Func<BaseItem, bool> filter = i => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); if (parentItem.IsFolder) @@ -130,7 +269,7 @@ namespace MediaBrowser.Api.UserLibrary if (!string.IsNullOrWhiteSpace(request.UserId)) { items = request.Recursive ? - folder.GetRecursiveChildren(user, filter) : + folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(filter); } else @@ -274,7 +413,7 @@ namespace MediaBrowser.Api.UserLibrary { items = items.Where(i => { - var userdata = UserDataRepository.GetUserData(user.Id, i.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(user, i); return userdata != null && userdata.Likes.HasValue && !userdata.Likes.Value; }); @@ -284,7 +423,7 @@ namespace MediaBrowser.Api.UserLibrary { items = items.Where(i => { - var userdata = UserDataRepository.GetUserData(user.Id, i.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(user, i); return userdata != null && userdata.Likes.HasValue && userdata.Likes.Value; }); @@ -294,7 +433,7 @@ namespace MediaBrowser.Api.UserLibrary { items = items.Where(i => { - var userdata = UserDataRepository.GetUserData(user.Id, i.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(user, i); var likes = userdata.Likes ?? false; var favorite = userdata.IsFavorite; @@ -307,7 +446,7 @@ namespace MediaBrowser.Api.UserLibrary { items = items.Where(i => { - var userdata = UserDataRepository.GetUserData(user.Id, i.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(user, i); return userdata != null && userdata.IsFavorite; }); @@ -326,12 +465,7 @@ namespace MediaBrowser.Api.UserLibrary var tags = request.GetTags(); if (tags.Length > 0) { - var hasTags = i as IHasTags; - if (hasTags == null) - { - return false; - } - if (!tags.Any(v => hasTags.Tags.Contains(v, StringComparer.OrdinalIgnoreCase))) + if (!tags.Any(v => i.Tags.Contains(v, StringComparer.OrdinalIgnoreCase))) { return false; } @@ -372,7 +506,7 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="includeItemTypes">The include item types.</param> /// <param name="mediaTypes">The media types.</param> /// <returns>IEnumerable{BaseItem}.</returns> - protected bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes) + private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes) { // Exclude item types if (excludeItemTypes.Length > 0) diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 6867f6308..3e9a541c0 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.Api.UserLibrary protected BaseItemsRequest() { EnableImages = true; + EnableTotalRecordCount = true; } /// <summary> @@ -99,12 +100,13 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? HasTvdbId { get; set; } - [ApiMember(Name = "IsYearMismatched", Description = "Optional filter by items that are potentially misidentified.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsYearMismatched { get; set; } - [ApiMember(Name = "IsInBoxSet", Description = "Optional filter by items that are in boxsets, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool? IsInBoxSet { get; set; } - + + public string ExcludeItemIds { get; set; } + + public bool EnableTotalRecordCount { get; set; } + /// <summary> /// Skips over a given number of items within the results. Use for paging. /// </summary> @@ -264,6 +266,8 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string Artists { get; set; } + public string ExcludeArtistIds { get; set; } + [ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] public string ArtistIds { get; set; } @@ -367,6 +371,11 @@ namespace MediaBrowser.Api.UserLibrary return (IncludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } + public string[] GetExcludeItemIds() + { + return (ExcludeItemIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + public string[] GetExcludeItemTypes() { return (ExcludeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Api/UserLibrary/GameGenresService.cs b/MediaBrowser.Api/UserLibrary/GameGenresService.cs index 58237f80f..a0883f98c 100644 --- a/MediaBrowser.Api/UserLibrary/GameGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GameGenresService.cs @@ -9,16 +9,13 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary { [Route("/GameGenres", "GET", Summary = "Gets all Game genres from a given item, folder, or the entire library")] public class GetGameGenres : GetItemsByName { - public GetGameGenres() - { - MediaTypes = MediaType.Game; - } } [Route("/GameGenres/{Name}", "GET", Summary = "Gets a Game genre, by name")] @@ -87,11 +84,16 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetGameGenres request) { - var result = GetResult(request); + var result = GetResultSlim(request); return ToOptimizedSerializedResultUsingCache(result); } + protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return LibraryManager.GetGameGenres(query); + } + /// <summary> /// Gets all items. /// </summary> @@ -100,22 +102,7 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items) { - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(name => - { - try - { - return LibraryManager.GetGameGenre(name); - } - catch (Exception ex) - { - Logger.ErrorException("Error getting genre {0}", ex, name); - return null; - } - }) - .Where(i => i != null); + throw new NotImplementedException(); } } } diff --git a/MediaBrowser.Api/UserLibrary/GenresService.cs b/MediaBrowser.Api/UserLibrary/GenresService.cs index d383bd0ad..57c11a1fa 100644 --- a/MediaBrowser.Api/UserLibrary/GenresService.cs +++ b/MediaBrowser.Api/UserLibrary/GenresService.cs @@ -9,6 +9,7 @@ using ServiceStack; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary { @@ -92,65 +93,37 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetGenres request) { - var result = GetResult(request); + var result = GetResultSlim(request); return ToOptimizedSerializedResultUsingCache(result); } - /// <summary> - /// Gets all items. - /// </summary> - /// <param name="request">The request.</param> - /// <param name="items">The items.</param> - /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> - protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items) + protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) { var viewType = GetParentItemViewType(request); if (string.Equals(viewType, CollectionType.Music) || string.Equals(viewType, CollectionType.MusicVideos)) { - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(name => LibraryManager.GetMusicGenre(name)); + return LibraryManager.GetMusicGenres(query); } if (string.Equals(viewType, CollectionType.Games)) { - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(name => - { - try - { - return LibraryManager.GetGameGenre(name); - } - catch (Exception ex) - { - Logger.ErrorException("Error getting genre {0}", ex, name); - return null; - } - }) - .Where(i => i != null); + return LibraryManager.GetGameGenres(query); } - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(name => - { - try - { - return LibraryManager.GetGenre(name); - } - catch (Exception ex) - { - Logger.ErrorException("Error getting genre {0}", ex, name); - return null; - } - }) - .Where(i => i != null); + return LibraryManager.GetGenres(query); + } + + /// <summary> + /// Gets all items. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="items">The items.</param> + /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> + protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items) + { + throw new NotImplementedException(); } } } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index cfdc40bb2..b4d88a7f8 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -34,7 +34,6 @@ namespace MediaBrowser.Api.UserLibrary /// The _user manager /// </summary> private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataRepository; /// <summary> /// The _library manager @@ -43,25 +42,37 @@ namespace MediaBrowser.Api.UserLibrary private readonly ILocalizationManager _localization; private readonly IDtoService _dtoService; - private readonly ICollectionManager _collectionManager; /// <summary> /// Initializes a new instance of the <see cref="ItemsService" /> class. /// </summary> /// <param name="userManager">The user manager.</param> /// <param name="libraryManager">The library manager.</param> - /// <param name="userDataRepository">The user data repository.</param> /// <param name="localization">The localization.</param> /// <param name="dtoService">The dto service.</param> - /// <param name="collectionManager">The collection manager.</param> - public ItemsService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, ILocalizationManager localization, IDtoService dtoService, ICollectionManager collectionManager) + public ItemsService(IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService) { + if (userManager == null) + { + throw new ArgumentNullException("userManager"); + } + if (libraryManager == null) + { + throw new ArgumentNullException("libraryManager"); + } + if (localization == null) + { + throw new ArgumentNullException("localization"); + } + if (dtoService == null) + { + throw new ArgumentNullException("dtoService"); + } + _userManager = userManager; _libraryManager = libraryManager; - _userDataRepository = userDataRepository; _localization = localization; _dtoService = dtoService; - _collectionManager = collectionManager; } /// <summary> @@ -71,6 +82,11 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public async Task<object> Get(GetItems request) { + if (request == null) + { + throw new ArgumentNullException("request"); + } + var result = await GetItems(request).ConfigureAwait(false); return ToOptimizedSerializedResultUsingCache(result); @@ -84,15 +100,32 @@ namespace MediaBrowser.Api.UserLibrary private async Task<ItemsResult> GetItems(GetItems request) { var user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(request.UserId) : null; + + var result = await GetQueryResult(request, user).ConfigureAwait(false); + + if (result == null) + { + throw new InvalidOperationException("GetItemsToSerialize returned null"); + } - var result = await GetItemsToSerialize(request, user).ConfigureAwait(false); + if (result.Items == null) + { + throw new InvalidOperationException("GetItemsToSerialize result.Items returned null"); + } var dtoOptions = GetDtoOptions(request); + var dtoList = await _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user).ConfigureAwait(false); + + if (dtoList == null) + { + throw new InvalidOperationException("GetBaseItemDtos returned null"); + } + return new ItemsResult { TotalRecordCount = result.TotalRecordCount, - Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user).ToArray() + Items = dtoList.ToArray() }; } @@ -102,7 +135,7 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="request">The request.</param> /// <param name="user">The user.</param> /// <returns>IEnumerable{BaseItem}.</returns> - private async Task<QueryResult<BaseItem>> GetItemsToSerialize(GetItems request, User user) + private async Task<QueryResult<BaseItem>> GetQueryResult(GetItems request, User user) { var item = string.IsNullOrEmpty(request.ParentId) ? user == null ? _libraryManager.RootFolder : user.RootFolder : @@ -119,11 +152,17 @@ namespace MediaBrowser.Api.UserLibrary // Default list type = children + var folder = item as Folder; + if (folder == null) + { + folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); + } + if (!string.IsNullOrEmpty(request.Ids)) { request.Recursive = true; var query = GetItemsQuery(request, user); - var result = await ((Folder)item).GetItems(query).ConfigureAwait(false); + var result = await folder.GetItems(query).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(request.SortBy)) { @@ -138,28 +177,22 @@ namespace MediaBrowser.Api.UserLibrary if (request.Recursive) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); - - return result; + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } if (user == null) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); - - return result; + return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); } var userRoot = item as UserRootFolder; if (userRoot == null) { - var result = await ((Folder)item).GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); - - return result; + return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } - IEnumerable<BaseItem> items = ((Folder)item).GetChildren(user, true); + IEnumerable<BaseItem> items = folder.GetChildren(user, true); var itemsArray = items.ToArray(); @@ -193,7 +226,6 @@ namespace MediaBrowser.Api.UserLibrary NameStartsWith = request.NameStartsWith, NameStartsWithOrGreater = request.NameStartsWithOrGreater, HasImdbId = request.HasImdbId, - IsYearMismatched = request.IsYearMismatched, IsPlaceHolder = request.IsPlaceHolder, IsLocked = request.IsLocked, IsInBoxSet = request.IsInBoxSet, @@ -230,7 +262,9 @@ namespace MediaBrowser.Api.UserLibrary ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId), ParentIndexNumber = request.ParentIndexNumber, AiredDuringSeason = request.AiredDuringSeason, - AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater + AlbumArtistStartsWithOrGreater = request.AlbumArtistStartsWithOrGreater, + EnableTotalRecordCount = request.EnableTotalRecordCount, + ExcludeItemIds = request.GetExcludeItemIds() }; if (!string.IsNullOrWhiteSpace(request.Ids)) @@ -306,17 +340,17 @@ namespace MediaBrowser.Api.UserLibrary { query.LocationTypes = request.LocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray(); } - + // Min official rating - if (!string.IsNullOrEmpty(request.MinOfficialRating)) + if (!string.IsNullOrWhiteSpace(request.MinOfficialRating)) { query.MinParentalRating = _localization.GetRatingLevel(request.MinOfficialRating); } // Max official rating - if (!string.IsNullOrEmpty(request.MaxOfficialRating)) + if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating)) { - query.MaxParentalRating = _localization.GetRatingLevel(request.MinOfficialRating); + query.MaxParentalRating = _localization.GetRatingLevel(request.MaxOfficialRating); } // Artists @@ -334,6 +368,12 @@ namespace MediaBrowser.Api.UserLibrary query.ArtistNames = request.Artists.Split('|'); } + // ExcludeArtistIds + if (!string.IsNullOrEmpty(request.ExcludeArtistIds)) + { + query.ExcludeArtistIds = request.ExcludeArtistIds.Split('|'); + } + // Albums if (!string.IsNullOrEmpty(request.Albums)) { diff --git a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs index 12cb62fac..887c99941 100644 --- a/MediaBrowser.Api/UserLibrary/MusicGenresService.cs +++ b/MediaBrowser.Api/UserLibrary/MusicGenresService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Dto; +using System; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -8,16 +9,14 @@ using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary { [Route("/MusicGenres", "GET", Summary = "Gets all music genres from a given item, folder, or the entire library")] public class GetMusicGenres : GetItemsByName { - public GetMusicGenres() - { - IncludeItemTypes = typeof(Audio).Name; - } } [Route("/MusicGenres/{Name}", "GET", Summary = "Gets a music genre, by name")] @@ -86,11 +85,16 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetMusicGenres request) { - var result = GetResult(request); + var result = GetResultSlim(request); return ToOptimizedSerializedResultUsingCache(result); } + protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return LibraryManager.GetMusicGenres(query); + } + /// <summary> /// Gets all items. /// </summary> @@ -99,10 +103,7 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>IEnumerable{Tuple{System.StringFunc{System.Int32}}}.</returns> protected override IEnumerable<BaseItem> GetAllItems(GetItemsByName request, IEnumerable<BaseItem> items) { - return items - .SelectMany(i => i.Genres) - .DistinctNames() - .Select(name => LibraryManager.GetMusicGenre(name)); + throw new NotImplementedException(); } } } diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index 94c391cb5..710d337ec 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -247,9 +247,9 @@ namespace MediaBrowser.Api.UserLibrary /// Posts the specified request. /// </summary> /// <param name="request">The request.</param> - public object Post(MarkPlayedItem request) + public async Task<object> Post(MarkPlayedItem request) { - var result = MarkPlayed(request).Result; + var result = await MarkPlayed(request).ConfigureAwait(false); return ToOptimizedResult(result); } @@ -429,7 +429,7 @@ namespace MediaBrowser.Api.UserLibrary await item.MarkUnplayed(user).ConfigureAwait(false); } - return _userDataRepository.GetUserDataDto(item, user); + return await _userDataRepository.GetUserDataDto(item, user).ConfigureAwait(false); } } }
\ No newline at end of file diff --git a/MediaBrowser.Api/UserLibrary/StudiosService.cs b/MediaBrowser.Api/UserLibrary/StudiosService.cs index 2cdabf721..9e9c25d78 100644 --- a/MediaBrowser.Api/UserLibrary/StudiosService.cs +++ b/MediaBrowser.Api/UserLibrary/StudiosService.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Dto; +using System; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -7,6 +8,7 @@ using MediaBrowser.Model.Dto; using ServiceStack; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Api.UserLibrary { @@ -90,11 +92,16 @@ namespace MediaBrowser.Api.UserLibrary /// <returns>System.Object.</returns> public object Get(GetStudios request) { - var result = GetResult(request); + var result = GetResultSlim(request); return ToOptimizedSerializedResultUsingCache(result); } + protected override QueryResult<Tuple<BaseItem, ItemCounts>> GetItems(GetItemsByName request, InternalItemsQuery query) + { + return LibraryManager.GetStudios(query); + } + /// <summary> /// Gets all items. /// </summary> diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index c2c481cb6..3be11bdc5 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -488,9 +488,9 @@ namespace MediaBrowser.Api.UserLibrary /// Posts the specified request. /// </summary> /// <param name="request">The request.</param> - public object Post(MarkFavoriteItem request) + public async Task<object> Post(MarkFavoriteItem request) { - var dto = MarkFavorite(request.UserId, request.Id, true).Result; + var dto = await MarkFavorite(request.UserId, request.Id, true).ConfigureAwait(false); return ToOptimizedResult(dto); } @@ -519,17 +519,15 @@ namespace MediaBrowser.Api.UserLibrary var item = string.IsNullOrEmpty(itemId) ? user.RootFolder : _libraryManager.GetItemById(itemId); - var key = item.GetUserDataKey(); - // Get the user data for this item - var data = _userDataRepository.GetUserData(user.Id, key); + var data = _userDataRepository.GetUserData(user, item); // Set favorite status data.IsFavorite = isFavorite; await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false); - return _userDataRepository.GetUserDataDto(item, user); + return await _userDataRepository.GetUserDataDto(item, user).ConfigureAwait(false); } /// <summary> @@ -547,9 +545,9 @@ namespace MediaBrowser.Api.UserLibrary /// Posts the specified request. /// </summary> /// <param name="request">The request.</param> - public object Post(UpdateUserItemRating request) + public async Task<object> Post(UpdateUserItemRating request) { - var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes).Result; + var dto = await UpdateUserItemRating(request.UserId, request.Id, request.Likes).ConfigureAwait(false); return ToOptimizedResult(dto); } @@ -567,16 +565,14 @@ namespace MediaBrowser.Api.UserLibrary var item = string.IsNullOrEmpty(itemId) ? user.RootFolder : _libraryManager.GetItemById(itemId); - var key = item.GetUserDataKey(); - // Get the user data for this item - var data = _userDataRepository.GetUserData(user.Id, key); + var data = _userDataRepository.GetUserData(user, item); data.Likes = likes; await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false); - return _userDataRepository.GetUserDataDto(item, user); + return await _userDataRepository.GetUserDataDto(item, user).ConfigureAwait(false); } } } diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 9b611c397..07ff36c41 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -385,7 +385,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("User not found"); } - await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false); + await _sessionMananger.RevokeUserTokens(user.Id.ToString("N"), null).ConfigureAwait(false); await _userManager.DeleteUser(user).ConfigureAwait(false); } @@ -465,6 +465,10 @@ namespace MediaBrowser.Api } await _userManager.ChangePassword(user, request.NewPassword).ConfigureAwait(false); + + var currentToken = AuthorizationContext.GetAuthorizationInfo(Request).Token; + + await _sessionMananger.RevokeUserTokens(user.Id.ToString("N"), currentToken).ConfigureAwait(false); } } @@ -602,7 +606,8 @@ namespace MediaBrowser.Api throw new ArgumentException("There must be at least one enabled user in the system."); } - await _sessionMananger.RevokeUserTokens(user.Id.ToString("N")).ConfigureAwait(false); + var currentToken = AuthorizationContext.GetAuthorizationInfo(Request).Token; + await _sessionMananger.RevokeUserTokens(user.Id.ToString("N"), currentToken).ConfigureAwait(false); } await _userManager.UpdateUserPolicy(request.Id, request).ConfigureAwait(false); diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index c6ec69c85..c8dbb7bb2 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -130,6 +130,7 @@ namespace MediaBrowser.Api var items = request.Ids.Split(',') .Select(i => new Guid(i)) .Select(i => _libraryManager.GetItemById(i)) + .OfType<Video>() .ToList(); if (items.Count < 2) @@ -137,14 +138,7 @@ namespace MediaBrowser.Api throw new ArgumentException("Please supply at least two videos to merge."); } - if (items.Any(i => !(i is Video))) - { - throw new ArgumentException("Only videos can be grouped together."); - } - - var videos = items.Cast<Video>().ToList(); - - var videosWithVersions = videos.Where(i => i.MediaSourceCount > 1) + var videosWithVersions = items.Where(i => i.MediaSourceCount > 1) .ToList(); if (videosWithVersions.Count > 1) @@ -156,7 +150,7 @@ namespace MediaBrowser.Api if (primaryVersion == null) { - primaryVersion = videos.OrderBy(i => + primaryVersion = items.OrderBy(i => { if (i.Video3DFormat.HasValue) { @@ -179,9 +173,9 @@ namespace MediaBrowser.Api }).First(); } - foreach (var item in videos.Where(i => i.Id != primaryVersion.Id)) + foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) { - item.PrimaryVersionId = primaryVersion.Id; + item.PrimaryVersionId = primaryVersion.Id.ToString("N"); await item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); |
