diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index bb9d2b8645..7c5f7cde40 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -192,13 +192,13 @@ namespace MediaBrowser.Api _activeTranscodingJobs.Add(job); - ReportTranscodingProgress(job, state, null, null, null, null); + ReportTranscodingProgress(job, state, null, null, null, null, null); return job; } } - public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded) + public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; @@ -208,6 +208,7 @@ namespace MediaBrowser.Api job.CompletionPercentage = percentComplete; job.TranscodingPositionTicks = ticks; job.BytesTranscoded = bytesTranscoded; + job.BitRate = bitRate; } var deviceId = state.Request.DeviceId; @@ -219,7 +220,7 @@ namespace MediaBrowser.Api _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { - Bitrate = state.TotalOutputBitrate, + Bitrate = bitRate ?? state.TotalOutputBitrate, AudioCodec = audioCodec, VideoCodec = videoCodec, Container = state.OutputContainer, @@ -694,6 +695,7 @@ namespace MediaBrowser.Api public long? BytesDownloaded { get; set; } public long? BytesTranscoded { get; set; } + public int? BitRate { get; set; } public long? TranscodingPositionTicks { get; set; } public long? DownloadPositionTicks { get; set; } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 44a367be09..3ff432d741 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -139,6 +139,10 @@ namespace MediaBrowser.Api { options.ImageTypeLimit = hasDtoOptions.ImageTypeLimit.Value; } + if (hasDtoOptions.EnableUserData.HasValue) + { + options.EnableUserData = hasDtoOptions.EnableUserData.Value; + } if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes)) { diff --git a/MediaBrowser.Api/IHasDtoOptions.cs b/MediaBrowser.Api/IHasDtoOptions.cs index dac366113c..6ed1670c21 100644 --- a/MediaBrowser.Api/IHasDtoOptions.cs +++ b/MediaBrowser.Api/IHasDtoOptions.cs @@ -4,6 +4,7 @@ namespace MediaBrowser.Api public interface IHasDtoOptions : IHasItemFields { bool? EnableImages { get; set; } + bool? EnableUserData { get; set; } int? ImageTypeLimit { get; set; } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 5866ad15bc..3280358dfa 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -573,11 +573,9 @@ namespace MediaBrowser.Api.Images var outputFormats = GetOutputFormats(request, imageInfo, cropwhitespace, supportedImageEnhancers); - var cacheGuid = new Guid(_imageProcessor.GetImageCacheTag(item, imageInfo, supportedImageEnhancers)); - TimeSpan? cacheDuration = null; - if (!string.IsNullOrEmpty(request.Tag) && cacheGuid == new Guid(request.Tag)) + if (!string.IsNullOrEmpty(request.Tag)) { cacheDuration = TimeSpan.FromDays(365); } diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index 3cf0d5d937..72966a7cdc 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -10,6 +10,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Configuration; namespace MediaBrowser.Api.Library { @@ -52,6 +55,8 @@ namespace MediaBrowser.Api.Library /// /// The path. public string[] Paths { get; set; } + + public LibraryOptions LibraryOptions { get; set; } } [Route("/Library/VirtualFolders", "DELETE")] @@ -136,6 +141,14 @@ namespace MediaBrowser.Api.Library public bool RefreshLibrary { get; set; } } + [Route("/Library/VirtualFolders/LibraryOptions", "POST")] + public class UpdateLibraryOptions : IReturnVoid + { + public string Id { get; set; } + + public LibraryOptions LibraryOptions { get; set; } + } + /// /// Class LibraryStructureService /// @@ -184,13 +197,22 @@ namespace MediaBrowser.Api.Library return ToOptimizedSerializedResultUsingCache(result); } + public void Post(UpdateLibraryOptions request) + { + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); + + collectionFolder.UpdateLibraryOptions(request.LibraryOptions); + } + /// /// Posts the specified request. /// /// The request. public void Post(AddVirtualFolder request) { - _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, request.RefreshLibrary); + var libraryOptions = request.LibraryOptions ?? new LibraryOptions(); + + _libraryManager.AddVirtualFolder(request.Name, request.CollectionType, request.Paths, libraryOptions, request.RefreshLibrary); } /// @@ -214,12 +236,12 @@ namespace MediaBrowser.Api.Library var currentPath = Path.Combine(rootFolderPath, request.Name); var newPath = Path.Combine(rootFolderPath, request.NewName); - if (!_fileSystem.DirectoryExists(currentPath)) + if (!_fileSystem.DirectoryExists(currentPath)) { throw new DirectoryNotFoundException("The media collection does not exist"); } - if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath)) + if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && _fileSystem.DirectoryExists(newPath)) { throw new ArgumentException("There is already a media collection with the name " + newPath + "."); } @@ -234,11 +256,11 @@ namespace MediaBrowser.Api.Library //Create an unique name var temporaryName = Guid.NewGuid().ToString(); var temporaryPath = Path.Combine(rootFolderPath, temporaryName); - _fileSystem.MoveDirectory(currentPath, temporaryPath); + _fileSystem.MoveDirectory(currentPath, temporaryPath); currentPath = temporaryPath; } - _fileSystem.MoveDirectory(currentPath, newPath); + _fileSystem.MoveDirectory(currentPath, newPath); } finally { diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 91157cd11f..545ac162ff 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -82,6 +82,9 @@ namespace MediaBrowser.Api.LiveTv [ApiMember(Name = "AddCurrentProgram", Description = "Optional. Adds current program info to each channel", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public bool AddCurrentProgram { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetChannels() { AddCurrentProgram = true; @@ -149,6 +152,9 @@ namespace MediaBrowser.Api.LiveTv public bool EnableTotalRecordCount { get; set; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetRecordings() { EnableTotalRecordCount = true; @@ -271,6 +277,9 @@ namespace MediaBrowser.Api.LiveTv [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; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + /// /// Fields to return within the items, in addition to basic information /// @@ -331,6 +340,9 @@ namespace MediaBrowser.Api.LiveTv /// The fields. [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; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/LiveTv/Programs/{Id}", "GET", Summary = "Gets a live tv program")] @@ -726,7 +738,12 @@ namespace MediaBrowser.Api.LiveTv var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId); - var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, GetDtoOptions(Request), user).ConfigureAwait(false)).ToArray(); + var options = GetDtoOptions(request); + RemoveFields(options); + + options.AddCurrentProgram = request.AddCurrentProgram; + + var returnArray = (await _dtoService.GetBaseItemDtos(channelResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult { @@ -737,6 +754,14 @@ namespace MediaBrowser.Api.LiveTv return ToOptimizedSerializedResultUsingCache(result); } + private void RemoveFields(DtoOptions options) + { + options.Fields.Remove(ItemFields.CanDelete); + options.Fields.Remove(ItemFields.CanDownload); + options.Fields.Remove(ItemFields.DisplayPreferencesId); + options.Fields.Remove(ItemFields.Etag); + } + public object Get(GetChannel request) { var user = string.IsNullOrWhiteSpace(request.UserId) ? null : _userManager.GetUserById(request.UserId); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 164d607d28..f52505c04b 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -22,6 +22,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; namespace MediaBrowser.Api.Playback { @@ -69,6 +71,9 @@ namespace MediaBrowser.Api.Playback protected IZipClient ZipClient { get; private set; } protected IJsonSerializer JsonSerializer { get; private set; } + public static IServerApplicationHost AppHost; + public static IHttpClient HttpClient; + /// /// Initializes a new instance of the class. /// @@ -1055,14 +1060,14 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; Logger.Info(commandLineLogMessage); - var logFilePrefix = "transcode"; + var logFilePrefix = "ffmpeg-transcode"; if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "directstream"; + logFilePrefix = "ffmpeg-directstream"; } else if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - logFilePrefix = "remux"; + logFilePrefix = "ffmpeg-remux"; } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); @@ -1112,28 +1117,30 @@ namespace MediaBrowser.Api.Playback } StartThrottler(state, transcodingJob); + ReportUsage(state); return transcodingJob; } private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { - if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.VideoType == VideoType.VideoFile && - !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + if (EnableThrottling(state) && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) - { - transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); - state.TranscodingThrottler.Start(); - } + transcodingJob.TranscodingThrottler = state.TranscodingThrottler = new TranscodingThrottler(transcodingJob, Logger, ServerConfigurationManager); + state.TranscodingThrottler.Start(); } } protected virtual bool EnableThrottling(StreamState state) { - return true; + // do not use throttling with hardware encoders + return state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && + state.IsInputVideo && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && + string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase); } private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target) @@ -1171,6 +1178,7 @@ namespace MediaBrowser.Api.Playback double? percent = null; TimeSpan? transcodingPosition = null; long? bytesTranscoded = null; + int? bitRate = null; var parts = line.Split(' '); @@ -1234,11 +1242,32 @@ namespace MediaBrowser.Api.Playback } } } + else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) + { + var rate = part.Split(new[] { '=' }, 2).Last(); + + int? scale = null; + if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) + { + scale = 1024; + rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + if (scale.HasValue) + { + float val; + + if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val)) + { + bitRate = (int)Math.Ceiling(val * scale.Value); + } + } + } } if (framerate.HasValue || percent.HasValue) { - ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded); + ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate); } } @@ -1588,6 +1617,10 @@ namespace MediaBrowser.Api.Playback videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); } } + else if (i == 29) + { + request.Tag = val; + } } } @@ -2192,6 +2225,121 @@ namespace MediaBrowser.Api.Playback } } + private async void ReportUsage(StreamState state) + { + try + { + await ReportUsageInternal(state).ConfigureAwait(false); + } + catch + { + + } + } + + private Task ReportUsageInternal(StreamState state) + { + if (!ServerConfigurationManager.Configuration.EnableAnonymousUsageReporting) + { + return Task.FromResult(true); + } + + if (!string.Equals(MediaEncoder.EncoderLocationType, "Default", StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(true); + } + + var dict = new Dictionary(); + + var outputAudio = GetAudioEncoder(state); + if (!string.IsNullOrWhiteSpace(outputAudio)) + { + dict["outputAudio"] = outputAudio; + } + + var outputVideo = GetVideoEncoder(state); + if (!string.IsNullOrWhiteSpace(outputVideo)) + { + dict["outputVideo"] = outputVideo; + } + + if (ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase) && + ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + return Task.FromResult(true); + } + + dict["id"] = AppHost.SystemId; + dict["type"] = state.VideoRequest == null ? "Audio" : "Video"; + + var audioStream = state.AudioStream; + if (audioStream != null && !string.IsNullOrWhiteSpace(audioStream.Codec)) + { + dict["inputAudio"] = audioStream.Codec; + } + + var videoStream = state.VideoStream; + if (videoStream != null && !string.IsNullOrWhiteSpace(videoStream.Codec)) + { + dict["inputVideo"] = videoStream.Codec; + } + + var cert = GetType().Assembly.GetModules().First().GetSignerCertificate(); + if (cert != null) + { + dict["assemblySig"] = cert.GetCertHashString(); + dict["certSubject"] = cert.Subject ?? string.Empty; + dict["certIssuer"] = cert.Issuer ?? string.Empty; + } + else + { + return Task.FromResult(true); + } + + if (state.SupportedAudioCodecs.Count > 0) + { + dict["supportedAudioCodecs"] = string.Join(",", state.SupportedAudioCodecs.ToArray()); + } + + var auth = AuthorizationContext.GetAuthorizationInfo(Request); + + dict["appName"] = auth.Client ?? string.Empty; + dict["appVersion"] = auth.Version ?? string.Empty; + dict["device"] = auth.Device ?? string.Empty; + dict["deviceId"] = auth.DeviceId ?? string.Empty; + dict["context"] = "streaming"; + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputAudio ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputAudio); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + if (!ServerConfigurationManager.Configuration.CodecsUsed.Contains(outputVideo ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + { + var list = ServerConfigurationManager.Configuration.CodecsUsed.ToList(); + list.Add(outputVideo); + ServerConfigurationManager.Configuration.CodecsUsed = list.ToArray(); + } + + ServerConfigurationManager.SaveConfiguration(); + + //Logger.Info(JsonSerializer.SerializeToString(dict)); + var options = new HttpRequestOptions() + { + Url = "https://mb3admin.com/admin/service/transcoding/report", + CancellationToken = CancellationToken.None, + LogRequest = false, + LogErrors = false + }; + options.RequestContent = JsonSerializer.SerializeToString(dict); + options.RequestContentType = "application/json"; + + return HttpClient.Post(options); + } + /// /// Adds the dlna headers. /// diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 0b989784c0..91e62b4e32 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -284,6 +284,13 @@ namespace MediaBrowser.Api.Playback options.ForceDirectPlay = true; } } + else if (item is Video) + { + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + 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) ? @@ -315,6 +322,13 @@ namespace MediaBrowser.Api.Playback options.ForceDirectStream = true; } } + else if (item is Video) + { + if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing) + { + 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) ? diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index d6dea0fe52..b8cb6b14f0 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -154,12 +154,20 @@ namespace MediaBrowser.Api.Playback.Progressive using (state) { + TimeSpan? cacheDuration = null; + + if (!string.IsNullOrEmpty(request.Tag)) + { + cacheDuration = TimeSpan.FromDays(365); + } + return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions { ResponseHeaders = responseHeaders, ContentType = contentType, IsHeadRequest = isHeadRequest, - Path = state.MediaPath + Path = state.MediaPath, + CacheDuration = cacheDuration }).ConfigureAwait(false); } @@ -362,9 +370,9 @@ namespace MediaBrowser.Api.Playback.Progressive outputHeaders[item.Key] = item.Value; } - Func streamWriter = stream => new ProgressiveFileCopier(FileSystem, job, Logger).StreamFile(outputPath, stream, CancellationToken.None); + var streamSource = new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, CancellationToken.None); - return ResultFactory.GetAsyncStreamWriter(streamWriter, outputHeaders); + return ResultFactory.GetAsyncStreamWriter(streamSource); } finally { @@ -383,7 +391,7 @@ namespace MediaBrowser.Api.Playback.Progressive if (totalBitrate > 0 && state.RunTimeTicks.HasValue) { - return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds); + return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds / 8); } return null; diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs index 63d71b85ef..0a9a446412 100644 --- a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs +++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs @@ -4,38 +4,55 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Net; +using System.Collections.Generic; +using ServiceStack.Web; namespace MediaBrowser.Api.Playback.Progressive { - public class ProgressiveFileCopier + public class ProgressiveFileCopier : IAsyncStreamSource, IHasOptions { private readonly IFileSystem _fileSystem; private readonly TranscodingJob _job; private readonly ILogger _logger; + private readonly string _path; + private readonly CancellationToken _cancellationToken; + private readonly Dictionary _outputHeaders; // 256k private const int BufferSize = 81920; private long _bytesWritten = 0; - public ProgressiveFileCopier(IFileSystem fileSystem, TranscodingJob job, ILogger logger) + public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) { _fileSystem = fileSystem; + _path = path; + _outputHeaders = outputHeaders; _job = job; _logger = logger; + _cancellationToken = cancellationToken; } - public async Task StreamFile(string path, Stream outputStream, CancellationToken cancellationToken) + public IDictionary Options + { + get + { + return _outputHeaders; + } + } + + public async Task WriteToAsync(Stream outputStream) { try { var eofCount = 0; - using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) + using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { while (eofCount < 15) { - var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, cancellationToken).ConfigureAwait(false); + var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false); //var position = fs.Position; //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); @@ -46,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Progressive { eofCount++; } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + await Task.Delay(100, _cancellationToken).ConfigureAwait(false); } else { diff --git a/MediaBrowser.Api/Playback/StreamRequest.cs b/MediaBrowser.Api/Playback/StreamRequest.cs index a8ca6aaa3b..e1a577f525 100644 --- a/MediaBrowser.Api/Playback/StreamRequest.cs +++ b/MediaBrowser.Api/Playback/StreamRequest.cs @@ -74,6 +74,7 @@ namespace MediaBrowser.Api.Playback public string Params { get; set; } public string PlaySessionId { get; set; } public string LiveStreamId { get; set; } + public string Tag { get; set; } } public class VideoStreamRequest : StreamRequest diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 604227a150..9693992882 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -72,7 +72,7 @@ namespace MediaBrowser.Api } [Route("/Playlists/{Id}/Items", "GET", Summary = "Gets the original items of a playlist")] - public class GetPlaylistItems : IReturn>, IHasItemFields + public class GetPlaylistItems : IReturn>, IHasDtoOptions { [ApiMember(Name = "Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } @@ -104,6 +104,18 @@ namespace MediaBrowser.Api /// The fields. [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; } + + [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 = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { 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; } } [Authenticated] diff --git a/MediaBrowser.Api/SimilarItemsHelper.cs b/MediaBrowser.Api/SimilarItemsHelper.cs index a1e47bd8fd..1621c80567 100644 --- a/MediaBrowser.Api/SimilarItemsHelper.cs +++ b/MediaBrowser.Api/SimilarItemsHelper.cs @@ -29,8 +29,20 @@ namespace MediaBrowser.Api public string ExcludeArtistIds { get; set; } } - public class BaseGetSimilarItems : IReturn, IHasItemFields + public class BaseGetSimilarItems : IReturn, IHasDtoOptions { + [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 = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { 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; } + /// /// Gets or sets the user id. /// diff --git a/MediaBrowser.Api/Sync/SyncHelper.cs b/MediaBrowser.Api/Sync/SyncHelper.cs index 0d3e8707db..2f857000c0 100644 --- a/MediaBrowser.Api/Sync/SyncHelper.cs +++ b/MediaBrowser.Api/Sync/SyncHelper.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Api.Sync } break; } - if (item.IsFolder && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre) + if (item.IsFolderItem && !item.IsMusicGenre && !item.IsArtist && !item.IsType("musicalbum") && !item.IsGameGenre) { options.Add(SyncJobOption.Quality); options.Add(SyncJobOption.Profile); @@ -44,7 +44,7 @@ namespace MediaBrowser.Api.Sync { if (item.SupportsSync ?? false) { - if (item.IsFolder || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson) + if (item.IsFolderItem || item.IsGameGenre || item.IsMusicGenre || item.IsGenre || item.IsArtist || item.IsStudio || item.IsPerson) { options.Add(SyncJobOption.SyncNewContent); options.Add(SyncJobOption.ItemLimit); diff --git a/MediaBrowser.Api/Sync/SyncService.cs b/MediaBrowser.Api/Sync/SyncService.cs index a15ce216f2..b9544d71ba 100644 --- a/MediaBrowser.Api/Sync/SyncService.cs +++ b/MediaBrowser.Api/Sync/SyncService.cs @@ -66,6 +66,7 @@ namespace MediaBrowser.Api.Sync public string Id { get; set; } } + [Route("/Sync/Items/Cancel", "POST", Summary = "Cancels items from a sync target")] [Route("/Sync/{TargetId}/Items", "DELETE", Summary = "Cancels items from a sync target")] public class CancelItems : IReturnVoid { @@ -211,7 +212,7 @@ namespace MediaBrowser.Api.Sync return ToOptimizedResult(result); } - public void Delete(CancelItems request) + public void Any(CancelItems request) { var itemIds = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index 3f248ea8f0..daaa6343d1 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -69,6 +69,9 @@ namespace MediaBrowser.Api [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; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")] @@ -117,6 +120,9 @@ namespace MediaBrowser.Api [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; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } } [Route("/Shows/{Id}/Similar", "GET", Summary = "Finds tv shows similar to a given one.")] @@ -184,6 +190,10 @@ namespace MediaBrowser.Api [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; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + } [Route("/Shows/{Id}/Seasons", "GET", Summary = "Gets seasons for a tv series")] @@ -226,6 +236,10 @@ namespace MediaBrowser.Api [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; } + + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + } /// @@ -409,23 +423,14 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("No series exists with Id " + request.Id); } - var seasons = series.GetSeasons(user); - - if (request.IsSpecialSeason.HasValue) + var seasons = (await series.GetItems(new InternalItemsQuery(user) { - var val = request.IsSpecialSeason.Value; + IsMissing = request.IsMissing, + IsVirtualUnaired = request.IsVirtualUnaired, + IsSpecialSeason = request.IsSpecialSeason, + AdjacentTo = request.AdjacentTo - seasons = seasons.Where(i => i.IsSpecialSeason == val); - } - - seasons = FilterVirtualSeasons(request, seasons); - - // This must be the last filter - if (!string.IsNullOrEmpty(request.AdjacentTo)) - { - seasons = UserViewBuilder.FilterForAdjacency(seasons, request.AdjacentTo) - .Cast(); - } + }).ConfigureAwait(false)).Items.OfType(); var dtoOptions = GetDtoOptions(request); @@ -439,23 +444,6 @@ namespace MediaBrowser.Api }; } - private IEnumerable FilterVirtualSeasons(GetSeasons request, IEnumerable items) - { - if (request.IsMissing.HasValue) - { - var val = request.IsMissing.Value; - items = items.Where(i => (i.IsMissingSeason) == val); - } - - if (request.IsVirtualUnaired.HasValue) - { - var val = request.IsVirtualUnaired.Value; - items = items.Where(i => i.IsVirtualUnaired == val); - } - - return items; - } - public async Task Get(GetEpisodes request) { var user = _userManager.GetUserById(request.UserId); @@ -490,7 +478,7 @@ namespace MediaBrowser.Api } else { - episodes = series.GetEpisodes(user, season); + episodes = series.GetSeasonEpisodes(user, season); } } else diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 852a3d59ee..94a6a7ef13 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -214,6 +214,7 @@ namespace MediaBrowser.Api.UserLibrary dto.AlbumCount = counts.AlbumCount; dto.SongCount = counts.SongCount; dto.GameCount = counts.GameCount; + dto.ArtistCount = counts.ArtistCount; } /// diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index 3e9a541c0c..96acb1f607 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -226,6 +226,9 @@ namespace MediaBrowser.Api.UserLibrary [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 = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { 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; } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index 97a81b7909..ce7905b42f 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -149,6 +149,24 @@ namespace MediaBrowser.Api.UserLibrary item = user == null ? _libraryManager.RootFolder : user.RootFolder; } + if (!string.IsNullOrEmpty(request.Ids)) + { + var query = GetItemsQuery(request, user); + var specificItems = _libraryManager.GetItemList(query).ToArray(); + if (query.SortBy.Length == 0) + { + var ids = query.ItemIds.ToList(); + + // Try to preserve order + specificItems = specificItems.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); + } + return new QueryResult + { + Items = specificItems.ToArray(), + TotalRecordCount = specificItems.Length + }; + } + // Default list type = children var folder = item as Folder; @@ -157,33 +175,11 @@ namespace MediaBrowser.Api.UserLibrary folder = user == null ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder(); } - if (!string.IsNullOrEmpty(request.Ids)) - { - request.Recursive = true; - var query = GetItemsQuery(request, user); - var result = await folder.GetItems(query).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(request.SortBy)) - { - var ids = query.ItemIds.ToList(); - - // Try to preserve order - result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id.ToString("N"))).ToArray(); - } - - return result; - } - - if (request.Recursive) + if (request.Recursive || !string.IsNullOrEmpty(request.Ids) || user == null) { return await folder.GetItems(GetItemsQuery(request, user)).ConfigureAwait(false); } - if (user == null) - { - return await folder.GetItems(GetItemsQuery(request, null)).ConfigureAwait(false); - } - var userRoot = item as UserRootFolder; if (userRoot == null) diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 3be11bdc56..c392ef4634 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -12,6 +12,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Controller.Providers; namespace MediaBrowser.Api.UserLibrary { @@ -244,6 +246,9 @@ namespace MediaBrowser.Api.UserLibrary [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; } + [ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] + public bool? EnableUserData { get; set; } + public GetLatestMedia() { Limit = 20; @@ -262,14 +267,16 @@ namespace MediaBrowser.Api.UserLibrary private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly IUserViewManager _userViewManager; + private readonly IFileSystem _fileSystem; - public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager) + public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager, IFileSystem fileSystem) { _userManager = userManager; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _dtoService = dtoService; _userViewManager = userViewManager; + _fileSystem = fileSystem; } /// @@ -426,12 +433,14 @@ namespace MediaBrowser.Api.UserLibrary /// /// The request. /// System.Object. - public object Get(GetItem request) + public async Task Get(GetItem request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _libraryManager.GetItemById(request.Id); + await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); + var dtoOptions = GetDtoOptions(request); var result = _dtoService.GetBaseItemDto(item, dtoOptions, user); @@ -439,6 +448,27 @@ namespace MediaBrowser.Api.UserLibrary return ToOptimizedSerializedResultUsingCache(result); } + private async Task RefreshItemOnDemandIfNeeded(BaseItem item) + { + if (item is Person) + { + var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary); + var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3; + + if (!hasMetdata) + { + var options = new MetadataRefreshOptions(_fileSystem) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = ImageRefreshMode.FullRefresh, + ForceSave = performFullRefresh + }; + + await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false); + } + } + } + /// /// Gets the specified request. /// diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index d627cc67ae..e69b649488 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -19,12 +19,16 @@ namespace MediaBrowser.Controller.Dto public bool EnableImages { get; set; } public bool AddProgramRecordingInfo { get; set; } public string DeviceId { get; set; } + public bool EnableUserData { get; set; } + public bool AddCurrentProgram { get; set; } public DtoOptions() { Fields = new List(); ImageTypeLimit = int.MaxValue; EnableImages = true; + EnableUserData = true; + AddCurrentProgram = true; Fields = Enum.GetNames(typeof (ItemFields)) .Select(i => (ItemFields) Enum.Parse(typeof (ItemFields), i, true)) diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 7b42310101..efc4502481 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -5,6 +5,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; using CommonIO; using MediaBrowser.Controller.Providers; @@ -67,6 +69,31 @@ namespace MediaBrowser.Controller.Entities return CreateResolveArgs(directoryService, true).FileSystemChildren; } + private List _childrenIds = null; + private readonly object _childIdsLock = new object(); + protected override IEnumerable LoadChildren() + { + lock (_childIdsLock) + { + if (_childrenIds == null || _childrenIds.Count == 0) + { + var list = base.LoadChildren().ToList(); + _childrenIds = list.Select(i => i.Id).ToList(); + return list; + } + + return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i != null).ToList(); + } + } + + private void ClearCache() + { + lock (_childIdsLock) + { + _childrenIds = null; + } + } + private bool _requiresRefresh; public override bool RequiresRefresh() { @@ -89,6 +116,8 @@ namespace MediaBrowser.Controller.Entities public override bool BeforeMetadataRefresh() { + ClearCache(); + var changed = base.BeforeMetadataRefresh() || _requiresRefresh; _requiresRefresh = false; return changed; @@ -96,9 +125,11 @@ namespace MediaBrowser.Controller.Entities private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations) { + ClearCache(); + var path = ContainingFolderPath; - var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths , directoryService) + var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService) { FileInfo = FileSystem.GetDirectoryInfo(path), Path = path, @@ -135,7 +166,22 @@ namespace MediaBrowser.Controller.Entities return args; } - + + protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) + { + return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); + } + + protected override async Task ValidateChildrenInternal(IProgress progress, CancellationToken cancellationToken, bool recursive, bool refreshChildMetadata, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService) + { + ClearCache(); + + await base.ValidateChildrenInternal(progress, cancellationToken, recursive, refreshChildMetadata, refreshOptions, directoryService) + .ConfigureAwait(false); + + ClearCache(); + } + /// /// Adds the virtual child. /// @@ -151,15 +197,6 @@ namespace MediaBrowser.Controller.Entities _virtualChildren.Add(child); } - /// - /// Get the children of this folder from the actual file system - /// - /// IEnumerable{BaseItem}. - protected override IEnumerable GetNonCachedChildren(IDirectoryService directoryService) - { - return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren); - } - /// /// Finds the virtual child. /// diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 1897511af7..1af55a389f 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -5,9 +5,11 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Runtime.Serialization; using System.Threading; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; namespace MediaBrowser.Controller.Entities.Audio @@ -47,7 +49,7 @@ namespace MediaBrowser.Controller.Entities.Audio } [IgnoreDataMember] - public override bool EnableForceSaveOnDateModifiedChange + public override bool EnableRefreshOnDateModifiedChange { get { return true; } } @@ -266,6 +268,11 @@ namespace MediaBrowser.Controller.Entities.Audio Size = i.Size }; + if (info.Protocol == MediaProtocol.File) + { + info.ETag = i.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + } + if (string.IsNullOrEmpty(info.Container)) { if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 6790a1bcf1..81d1deaa23 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -169,13 +169,9 @@ namespace MediaBrowser.Controller.Entities.Audio list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); - } + return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); } protected override bool GetBlockUnratedValue(UserPolicy config) { @@ -274,5 +270,54 @@ namespace MediaBrowser.Controller.Entities.Audio return false; } } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + if (IsAccessedByName) + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index 798bc79fbb..bd991d9f47 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -18,13 +18,9 @@ namespace MediaBrowser.Controller.Entities.Audio list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics()); return list; } - - public override string PresentationUniqueKey + public override string CreatePresentationUniqueKey() { - get - { - return GetUserDataKeys()[0]; - } + return GetUserDataKeys()[0]; } [IgnoreDataMember] @@ -96,5 +92,48 @@ namespace MediaBrowser.Controller.Entities.Audio return LibraryManager.GetItemList(query); } + + public static string GetPath(string name, bool normalizeName = true) + { + // Trim the period at the end because windows will have a hard time with that + var validName = normalizeName ? + FileSystem.GetValidFilename(name).Trim().TrimEnd('.') : + name; + + return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName); + } + + private string GetRebasedPath() + { + return GetPath(System.IO.Path.GetFileName(Path), false); + } + + public override bool RequiresRefresh() + { + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Logger.Debug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath); + return true; + } + return base.RequiresRefresh(); + } + + /// + /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// + public override bool BeforeMetadataRefresh() + { + var hasChanges = base.BeforeMetadataRefresh(); + + var newPath = GetRebasedPath(); + if (!string.Equals(Path, newPath, StringComparison.Ordinal)) + { + Path = newPath; + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8d00f38beb..cbbb9a89a3 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities public DateTime DateLastRefreshed { get; set; } [IgnoreDataMember] - public virtual bool EnableForceSaveOnDateModifiedChange + public virtual bool EnableRefreshOnDateModifiedChange { get { return false; } } @@ -951,7 +951,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => !i.IsDirectory && string.Equals(FileSystem.GetFileNameWithoutExtension(i), ThemeSongFilename, StringComparison.OrdinalIgnoreCase)) ); - return LibraryManager.ResolvePaths(files, directoryService, null) + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType() .Select(audio => { @@ -981,7 +981,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => string.Equals(i.Name, ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => directoryService.GetFiles(i.FullName)); - return LibraryManager.ResolvePaths(files, directoryService, null) + return LibraryManager.ResolvePaths(files, directoryService, null, new LibraryOptions()) .OfType