diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index d3a4558c8..a59b5f351 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -550,9 +550,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); diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 421ccdb5d..253e3d8e4 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -1518,6 +1518,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) { @@ -1528,7 +1535,7 @@ namespace MediaBrowser.Api.Playback } } } - else if (i == 26) + else if (i == 27) { request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture); } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index c2183ad7b..1382527e2 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -98,6 +98,9 @@ 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; } } [Route("/Videos/{Id}/{MediaSourceId}/Subtitles/{Index}/subtitles.m3u8", "GET", Summary = "Gets an HLS subtitle playlist.")] @@ -175,7 +178,7 @@ namespace MediaBrowser.Api.Subtitles 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,StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}", positionTicks.ToString(CultureInfo.InvariantCulture), endPositionTicks.ToString(CultureInfo.InvariantCulture), accessToken); @@ -222,6 +225,7 @@ namespace MediaBrowser.Api.Subtitles request.Format, request.StartPositionTicks, request.EndPositionTicks, + request.CopyTimestamps, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index d4a8b0730..2bde80641 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -332,13 +332,14 @@ namespace MediaBrowser.Controller.Entities private QueryResult GetMusicAlbumArtists(Folder parent, User user, InternalItemsQuery query) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = parent.QueryRecursive(new InternalItemsQuery(user) { Recursive = true, ParentId = parent.Id, - IncludeItemTypes = new[] { typeof(Audio.Audio).Name } + IncludeItemTypes = new[] { typeof(Audio.Audio).Name }, + EnableTotalRecordCount = false - }).Cast(); + }).Items.Cast(); var artists = _libraryManager.GetAlbumArtists(items); @@ -347,13 +348,14 @@ namespace MediaBrowser.Controller.Entities private QueryResult GetMusicArtists(Folder parent, User user, InternalItemsQuery query) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = parent.QueryRecursive(new InternalItemsQuery(user) { Recursive = true, ParentId = parent.Id, - IncludeItemTypes = new[] { typeof(Audio.Audio).Name, typeof(MusicVideo).Name } + IncludeItemTypes = new[] { typeof(Audio.Audio).Name, typeof(MusicVideo).Name }, + EnableTotalRecordCount = false - }).Cast(); + }).Items.Cast(); var artists = _libraryManager.GetArtists(items); @@ -362,13 +364,14 @@ namespace MediaBrowser.Controller.Entities private QueryResult GetFavoriteArtists(Folder parent, User user, InternalItemsQuery query) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) + var items = parent.QueryRecursive(new InternalItemsQuery(user) { Recursive = true, ParentId = parent.Id, - IncludeItemTypes = new[] { typeof(Audio.Audio).Name } + IncludeItemTypes = new[] { typeof(Audio.Audio).Name }, + EnableTotalRecordCount = false - }).Cast(); + }).Items.Cast(); var artists = _libraryManager.GetAlbumArtists(items).Where(i => _userDataManager.GetUserData(user, i).IsFavorite); diff --git a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs index e538b84d8..44489cbf5 100644 --- a/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/ISubtitleEncoder.cs @@ -10,13 +10,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Gets the subtitles. /// - /// The item identifier. - /// The media source identifier. - /// Index of the subtitle stream. - /// The output format. - /// The start time ticks. - /// The end time ticks. - /// The cancellation token. /// Task{Stream}. Task GetSubtitles(string itemId, string mediaSourceId, @@ -24,6 +17,7 @@ namespace MediaBrowser.Controller.MediaEncoding string outputFormat, long startTimeTicks, long? endTimeTicks, + bool preserveOriginalTimestamps, CancellationToken cancellationToken); /// diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8c33cc7c0..25adbcdb0 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -58,6 +58,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string outputFormat, long startTimeTicks, long? endTimeTicks, + bool preserveOriginalTimestamps, CancellationToken cancellationToken) { var ms = new MemoryStream(); @@ -68,7 +69,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var trackInfo = reader.Parse(stream, cancellationToken); - FilterEvents(trackInfo, startTimeTicks, endTimeTicks, false); + FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); var writer = GetWriter(outputFormat); @@ -116,6 +117,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string outputFormat, long startTimeTicks, long? endTimeTicks, + bool preserveOriginalTimestamps, CancellationToken cancellationToken) { var subtitle = await GetSubtitleStream(itemId, mediaSourceId, subtitleStreamIndex, cancellationToken) @@ -130,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles using (var stream = subtitle.Item1) { - return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, cancellationToken).ConfigureAwait(false); + return await ConvertSubtitles(stream, inputFormat, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimestamps, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Server.Implementations/IO/FileRefresher.cs b/MediaBrowser.Server.Implementations/IO/FileRefresher.cs index 74dfbc679..18c52ab29 100644 --- a/MediaBrowser.Server.Implementations/IO/FileRefresher.cs +++ b/MediaBrowser.Server.Implementations/IO/FileRefresher.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Events; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -24,9 +25,14 @@ namespace MediaBrowser.Server.Implementations.IO private readonly List _affectedPaths = new List(); private Timer _timer; private readonly object _timerLock = new object(); + public string Path { get; private set; } + + public event EventHandler Completed; public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger) { + logger.Debug("New file refresher created for {0}", path); + Path = path; _affectedPaths.Add(path); _fileSystem = fileSystem; @@ -36,7 +42,24 @@ namespace MediaBrowser.Server.Implementations.IO Logger = logger; } - private void RestartTimer() + private void AddAffectedPath(string path) + { + if (!_affectedPaths.Contains(path, StringComparer.Ordinal)) + { + _affectedPaths.Add(path); + } + } + + public void AddPath(string path) + { + lock (_timerLock) + { + AddAffectedPath(path); + } + RestartTimer(); + } + + public void RestartTimer() { lock (_timerLock) { @@ -51,6 +74,23 @@ namespace MediaBrowser.Server.Implementations.IO } } + public void ResetPath(string path, string affectedFile) + { + lock (_timerLock) + { + Logger.Debug("Resetting file refresher from {0} to {1}", Path, path); + + Path = path; + AddAffectedPath(path); + + if (!string.IsNullOrWhiteSpace(affectedFile)) + { + AddAffectedPath(affectedFile); + } + } + RestartTimer(); + } + private async void OnTimerCallback(object state) { // Extend the timer as long as any of the paths are still being written to. @@ -64,10 +104,11 @@ namespace MediaBrowser.Server.Implementations.IO Logger.Debug("Timer stopped."); DisposeTimer(); + EventHelper.FireEventIfNotNull(Completed, this, EventArgs.Empty, Logger); try { - await ProcessPathChanges(_affectedPaths).ConfigureAwait(false); + await ProcessPathChanges(_affectedPaths.ToList()).ConfigureAwait(false); } catch (Exception ex) { @@ -130,7 +171,7 @@ namespace MediaBrowser.Server.Implementations.IO { item = LibraryManager.FindByPath(path, null); - path = Path.GetDirectoryName(path); + path = System.IO.Path.GetDirectoryName(path); } if (item != null) @@ -222,7 +263,7 @@ namespace MediaBrowser.Server.Implementations.IO } } - public void DisposeTimer() + private void DisposeTimer() { lock (_timerLock) { diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 09ca134d1..0690d62dd 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -26,13 +26,9 @@ namespace MediaBrowser.Server.Implementations.IO /// private readonly ConcurrentDictionary _fileSystemWatchers = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// - /// The update timer - /// - private Timer _updateTimer; - /// /// The affected paths /// - private readonly ConcurrentDictionary _affectedPaths = new ConcurrentDictionary(); + private readonly List _activeRefreshers = new List(); /// /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. @@ -44,8 +40,8 @@ namespace MediaBrowser.Server.Implementations.IO /// private readonly IReadOnlyList _alwaysIgnoreFiles = new List { - "thumbs.db", - "small.jpg", + "thumbs.db", + "small.jpg", "albumart.jpg", // WMC temp recording directories that will constantly be written to @@ -53,11 +49,6 @@ namespace MediaBrowser.Server.Implementations.IO "TempSBE" }; - /// - /// The timer lock - /// - private readonly object _timerLock = new object(); - /// /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// @@ -463,226 +454,58 @@ namespace MediaBrowser.Server.Implementations.IO if (monitorPath) { // Avoid implicitly captured closure - var affectedPath = path; - _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath); - } - - RestartTimer(); - } - - private void RestartTimer() - { - lock (_timerLock) - { - if (_updateTimer == null) - { - _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); - } - else - { - _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); - } + CreateRefresher(path); } } - /// - /// Timers the stopped. - /// - /// The state info. - private async void TimerStopped(object stateInfo) + private void CreateRefresher(string path) { - // Extend the timer as long as any of the paths are still being written to. - if (_affectedPaths.Any(p => IsFileLocked(p.Key))) + var parentPath = Path.GetDirectoryName(path); + + lock (_activeRefreshers) { - Logger.Info("Timer extended."); - RestartTimer(); - return; - } - - Logger.Debug("Timer stopped."); - - DisposeTimer(); - - var paths = _affectedPaths.Keys.ToList(); - _affectedPaths.Clear(); - - try - { - await ProcessPathChanges(paths).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error processing directory changes", ex); - } - } - - private bool IsFileLocked(string path) - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - { - // Causing lockups on linux - return false; - } - - try - { - var data = _fileSystem.GetFileSystemInfo(path); - - if (!data.Exists - || data.IsDirectory - - // Opening a writable stream will fail with readonly files - || data.Attributes.HasFlag(FileAttributes.ReadOnly)) + var refreshers = _activeRefreshers.ToList(); + foreach (var refresher in refreshers) { - return false; - } - } - catch (IOException) - { - return false; - } - catch (Exception ex) - { - Logger.ErrorException("Error getting file system info for: {0}", ex, path); - return false; - } - - // In order to determine if the file is being written to, we have to request write access - // But if the server only has readonly access, this is going to cause this entire algorithm to fail - // So we'll take a best guess about our access level - var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta - ? FileAccess.ReadWrite - : FileAccess.Read; - - try - { - using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite)) - { - if (_updateTimer != null) + // Path is already being refreshed + if (string.Equals(path, refresher.Path, StringComparison.Ordinal)) { - //file is not locked - return false; + refresher.RestartTimer(); + return; + } + + // Parent folder is already being refreshed + if (_fileSystem.ContainsSubPath(refresher.Path, path)) + { + refresher.AddPath(path); + return; + } + + // New path is a parent + if (_fileSystem.ContainsSubPath(path, refresher.Path)) + { + refresher.ResetPath(path, null); + return; + } + + // They are siblings. Rebase the refresher to the parent folder. + if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + { + refresher.ResetPath(parentPath, path); + return; } } - } - catch (DirectoryNotFoundException) - { - // File may have been deleted - return false; - } - catch (FileNotFoundException) - { - // File may have been deleted - return false; - } - catch (IOException) - { - //the file is unavailable because it is: - //still being written to - //or being processed by another thread - //or does not exist (has already been processed) - Logger.Debug("{0} is locked.", path); - return true; - } - catch (Exception ex) - { - Logger.ErrorException("Error determining if file is locked: {0}", ex, path); - return false; - } - return false; - } - - private void DisposeTimer() - { - lock (_timerLock) - { - if (_updateTimer != null) - { - _updateTimer.Dispose(); - _updateTimer = null; - } + var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger); + newRefresher.Completed += NewRefresher_Completed; + _activeRefreshers.Add(newRefresher); } } - /// - /// Processes the path changes. - /// - /// The paths. - /// Task. - private async Task ProcessPathChanges(List paths) + private void NewRefresher_Completed(object sender, EventArgs e) { - var itemsToRefresh = paths - .Select(GetAffectedBaseItem) - .Where(item => item != null) - .Distinct() - .ToList(); - - foreach (var p in paths) - { - Logger.Info(p + " reports change."); - } - - // If the root folder changed, run the library task so the user can see it - if (itemsToRefresh.Any(i => i is AggregateFolder)) - { - TaskManager.CancelIfRunningAndQueue(); - return; - } - - foreach (var item in itemsToRefresh) - { - Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); - - try - { - await item.ChangedExternally().ConfigureAwait(false); - } - catch (IOException ex) - { - // For now swallow and log. - // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) - // Should we remove it from it's parent? - Logger.ErrorException("Error refreshing {0}", ex, item.Name); - } - catch (Exception ex) - { - Logger.ErrorException("Error refreshing {0}", ex, item.Name); - } - } - } - - /// - /// Gets the affected base item. - /// - /// The path. - /// BaseItem. - private BaseItem GetAffectedBaseItem(string path) - { - BaseItem item = null; - - while (item == null && !string.IsNullOrEmpty(path)) - { - item = LibraryManager.FindByPath(path, null); - - path = Path.GetDirectoryName(path); - } - - if (item != null) - { - // If the item has been deleted find the first valid parent that still exists - while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) - { - item = item.GetParent(); - - if (item == null) - { - break; - } - } - } - - return item; + var refresher = (FileRefresher)sender; + DisposeRefresher(refresher); } /// @@ -713,10 +536,29 @@ namespace MediaBrowser.Server.Implementations.IO watcher.Dispose(); } - DisposeTimer(); - _fileSystemWatchers.Clear(); - _affectedPaths.Clear(); + DisposeRefreshers(); + } + + private void DisposeRefresher(FileRefresher refresher) + { + lock (_activeRefreshers) + { + refresher.Dispose(); + _activeRefreshers.Remove(refresher); + } + } + + private void DisposeRefreshers() + { + lock (_activeRefreshers) + { + foreach (var refresher in _activeRefreshers.ToList()) + { + refresher.Dispose(); + } + _activeRefreshers.Clear(); + } } /// diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index bbdc4ab0d..e126e5411 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1959,10 +1959,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null); - info.Days = new List - { - program.StartDate.ToLocalTime().DayOfWeek - }; + info.Days = defaults.Item1.Days; info.DayPattern = _tvDtoService.GetDayPattern(info.Days); diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs index bbba06870..398dcc86b 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs @@ -748,7 +748,7 @@ namespace MediaBrowser.Server.Implementations.Sync _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, cancellationToken).ConfigureAwait(false)) + using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, false, cancellationToken).ConfigureAwait(false)) { using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) { diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 1569c1fc5..904953cfc 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -356,9 +356,6 @@ namespace MediaBrowser.WebDashboard.Api DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "meteor"); DeleteFoldersByName(Path.Combine(bowerPath, "Sortable"), "st"); DeleteFoldersByName(Path.Combine(bowerPath, "Swiper"), "src"); - DeleteFoldersByName(Path.Combine(bowerPath, "material-design-lite"), "src"); - DeleteFoldersByName(Path.Combine(bowerPath, "material-design-lite"), "utils"); - _fileSystem.DeleteFile(Path.Combine(bowerPath, "material-design-lite", "gulpfile.babel.js")); _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked"), true); _fileSystem.DeleteDirectory(Path.Combine(bowerPath, "marked-element"), true);