From 9d40b684bf6af4e987e226c78c11d6daf6f5cd9b Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 22 Jan 2014 12:05:06 -0500 Subject: [PATCH] #680 - episode organization --- ...MediaBrowser.Common.Implementations.csproj | 12 +- .../packages.config | 2 +- .../IFileOrganizationService.cs | 36 +- .../IFileOrganizationRepository.cs | 6 + .../FileOrganization/FileOrganizationQuery.cs | 15 + .../TV/SeriesPostScanTask.cs | 8 +- .../FileOrganization/EpisodeFileOrganizer.cs | 357 +++++++++++ .../FileOrganizationService.cs | 62 +- .../FileOrganization/NameUtils.cs | 92 +++ .../OrganizerScheduledTask.cs | 21 +- .../FileOrganization/TvFileSorter.cs | 563 ------------------ .../FileOrganization/TvFolderOrganizer.cs | 176 ++++++ .../Library/Resolvers/TV/EpisodeResolver.cs | 13 +- ...MediaBrowser.Server.Implementations.csproj | 4 +- .../MediaEncoder/MediaEncoder.cs | 2 +- .../SqliteFileOrganizationRepository.cs | 53 +- .../ApplicationHost.cs | 2 +- .../MediaBrowser.ServerApplication.csproj | 9 +- .../packages.config | 2 +- MediaBrowser.WebDashboard/ApiClient.js | 29 +- MediaBrowser.WebDashboard/packages.config | 2 +- 21 files changed, 822 insertions(+), 644 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs delete mode 100644 MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs diff --git a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj index 66567fc161..69c8809166 100644 --- a/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj +++ b/MediaBrowser.Common.Implementations/MediaBrowser.Common.Implementations.csproj @@ -48,8 +48,13 @@ Always - - ..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll + + False + ..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.dll + + + False + ..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.Diagnostics.dll @@ -67,9 +72,6 @@ ..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll - - ..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll - diff --git a/MediaBrowser.Common.Implementations/packages.config b/MediaBrowser.Common.Implementations/packages.config index 81647c1146..e04fecc1d9 100644 --- a/MediaBrowser.Common.Implementations/packages.config +++ b/MediaBrowser.Common.Implementations/packages.config @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs index 1978402144..6c0a73b8a1 100644 --- a/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs +++ b/MediaBrowser.Controller/FileOrganization/IFileOrganizationService.cs @@ -12,14 +12,6 @@ namespace MediaBrowser.Controller.FileOrganization /// void BeginProcessNewFiles(); - /// - /// Saves the result. - /// - /// The result. - /// The cancellation token. - /// Task. - Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken); - /// /// Deletes the original file. /// @@ -27,12 +19,25 @@ namespace MediaBrowser.Controller.FileOrganization /// Task. Task DeleteOriginalFile(string resultId); + /// + /// Clears the log. + /// + /// Task. + Task ClearLog(); + /// /// Performs the organization. /// /// The result identifier. /// Task. Task PerformOrganization(string resultId); + + /// + /// Performs the episode organization. + /// + /// The request. + /// Task. + Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request); /// /// Gets the results. @@ -40,5 +45,20 @@ namespace MediaBrowser.Controller.FileOrganization /// The query. /// IEnumerable{FileOrganizationResult}. QueryResult GetResults(FileOrganizationResultQuery query); + + /// + /// Gets the result. + /// + /// The identifier. + /// FileOrganizationResult. + FileOrganizationResult GetResult(string id); + + /// + /// Saves the result. + /// + /// The result. + /// The cancellation token. + /// Task. + Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Persistence/IFileOrganizationRepository.cs b/MediaBrowser.Controller/Persistence/IFileOrganizationRepository.cs index 14d2081bb1..f71784d823 100644 --- a/MediaBrowser.Controller/Persistence/IFileOrganizationRepository.cs +++ b/MediaBrowser.Controller/Persistence/IFileOrganizationRepository.cs @@ -35,5 +35,11 @@ namespace MediaBrowser.Controller.Persistence /// The query. /// IEnumerable{FileOrganizationResult}. QueryResult GetResults(FileOrganizationResultQuery query); + + /// + /// Deletes all. + /// + /// Task. + Task DeleteAll(); } } diff --git a/MediaBrowser.Model/FileOrganization/FileOrganizationQuery.cs b/MediaBrowser.Model/FileOrganization/FileOrganizationQuery.cs index 18287534ea..ce57507571 100644 --- a/MediaBrowser.Model/FileOrganization/FileOrganizationQuery.cs +++ b/MediaBrowser.Model/FileOrganization/FileOrganizationQuery.cs @@ -15,4 +15,19 @@ namespace MediaBrowser.Model.FileOrganization /// The limit. public int? Limit { get; set; } } + + public class EpisodeFileOrganizationRequest + { + public string ResultId { get; set; } + + public string SeriesId { get; set; } + + public int SeasonNumber { get; set; } + + public int EpisodeNumber { get; set; } + + public int? EndingEpisodeNumber { get; set; } + + public bool RememberCorrection { get; set; } + } } diff --git a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs index adc61fb736..6d2ece19c7 100644 --- a/MediaBrowser.Providers/TV/SeriesPostScanTask.cs +++ b/MediaBrowser.Providers/TV/SeriesPostScanTask.cs @@ -39,12 +39,6 @@ namespace MediaBrowser.Providers.TV private async Task RunInternal(IProgress progress, CancellationToken cancellationToken) { - if (!_config.Configuration.EnableInternetProviders) - { - progress.Report(100); - return; - } - var seriesList = _libraryManager.RootFolder .RecursiveChildren .OfType() @@ -288,7 +282,7 @@ namespace MediaBrowser.Providers.TV return hasChanges; } - private Series DetermineAppropriateSeries(IEnumerable series, int seasonNumber) + private Series DetermineAppropriateSeries(List series, int seasonNumber) { return series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == seasonNumber)) ?? series.FirstOrDefault(s => s.RecursiveChildren.OfType().Any(season => season.IndexNumber == 1)) ?? diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs new file mode 100644 index 0000000000..f9f54199f5 --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System.Globalization; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public class EpisodeFileOrganizer + { + private readonly IDirectoryWatchers _directoryWatchers; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IFileOrganizationService _organizationService; + private readonly IServerConfigurationManager _config; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers) + { + _organizationService = organizationService; + _config = config; + _fileSystem = fileSystem; + _logger = logger; + _libraryManager = libraryManager; + _directoryWatchers = directoryWatchers; + } + + public async Task OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting) + { + _logger.Info("Sorting file {0}", path); + + var result = new FileOrganizationResult + { + Date = DateTime.UtcNow, + OriginalPath = path, + OriginalFileName = Path.GetFileName(path), + Type = FileOrganizerType.Episode + }; + + var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path); + + if (!string.IsNullOrEmpty(seriesName)) + { + var season = TVUtils.GetSeasonNumberFromEpisodeFile(path); + + result.ExtractedSeasonNumber = season; + + if (season.HasValue) + { + // Passing in true will include a few extra regex's + var episode = TVUtils.GetEpisodeNumberFromFile(path, true); + + result.ExtractedEpisodeNumber = episode; + + if (episode.HasValue) + { + _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode); + + var endingEpisodeNumber = TVUtils.GetEndingEpisodeNumberFromFile(path); + + result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; + + OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, result); + } + else + { + var msg = string.Format("Unable to determine episode number from {0}", path); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + } + } + else + { + var msg = string.Format("Unable to determine season number from {0}", path); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + } + } + else + { + var msg = string.Format("Unable to determine series name from {0}", path); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + } + + await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); + + return result; + } + + public async Task OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options) + { + var result = _organizationService.GetResult(request.ResultId); + + var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); + + OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, _config.Configuration.TvFileOrganizationOptions, true, result); + + await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); + + return result; + } + + private void OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result) + { + var series = GetMatchingSeries(seriesName, result); + + if (series == null) + { + var msg = string.Format("Unable to find series in library matching name {0}", seriesName); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + return; + } + + OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, result); + } + + private void OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result) + { + _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); + + // Proceed to sort the file + var newPath = GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options); + + if (string.IsNullOrEmpty(newPath)) + { + var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + return; + } + + _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); + result.TargetPath = newPath; + + var existing = GetDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber); + + if (!overwriteExisting && existing.Count > 0) + { + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = string.Empty; + return; + } + + PerformFileSorting(options, result); + } + + private List GetDuplicatePaths(string targetPath, Series series, int seasonNumber, int episodeNumber) + { + var list = new List(); + + if (File.Exists(targetPath)) + { + list.Add(targetPath); + } + + return list; + } + + private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) + { + _directoryWatchers.TemporarilyIgnore(result.TargetPath); + + Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); + + var copy = File.Exists(result.TargetPath); + + try + { + if (copy) + { + File.Copy(result.OriginalPath, result.TargetPath, true); + } + else + { + File.Move(result.OriginalPath, result.TargetPath); + } + + result.Status = FileSortingStatus.Success; + result.StatusMessage = string.Empty; + } + catch (Exception ex) + { + var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath); + + result.Status = FileSortingStatus.Failure; + result.StatusMessage = errorMsg; + _logger.ErrorException(errorMsg, ex); + + return; + } + finally + { + _directoryWatchers.RemoveTempIgnore(result.TargetPath); + } + + if (copy) + { + try + { + File.Delete(result.OriginalPath); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); + } + } + } + + private Series GetMatchingSeries(string seriesName, FileOrganizationResult result) + { + int? yearInName; + var nameWithoutYear = seriesName; + NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out yearInName); + + result.ExtractedName = nameWithoutYear; + result.ExtractedYear = yearInName; + + return _libraryManager.RootFolder.RecursiveChildren + .OfType() + .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i)) + .Where(i => i.Item2 > 0) + .OrderByDescending(i => i.Item2) + .Select(i => i.Item1) + .FirstOrDefault(); + } + + /// + /// Gets the new path. + /// + /// The source path. + /// The series. + /// The season number. + /// The episode number. + /// The ending episode number. + /// The options. + /// System.String. + private string GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options) + { + // If season and episode numbers match + var currentEpisodes = series.RecursiveChildren.OfType() + .Where(i => i.IndexNumber.HasValue && + i.IndexNumber.Value == episodeNumber && + i.ParentIndexNumber.HasValue && + i.ParentIndexNumber.Value == seasonNumber) + .ToList(); + + if (currentEpisodes.Count == 0) + { + return null; + } + + var newPath = GetSeasonFolderPath(series, seasonNumber, options); + + var episode = currentEpisodes.First(); + + var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options); + + newPath = Path.Combine(newPath, episodeFileName); + + return newPath; + } + + /// + /// Gets the season folder path. + /// + /// The series. + /// The season number. + /// The options. + /// System.String. + private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) + { + // If there's already a season folder, use that + var season = series + .RecursiveChildren + .OfType() + .FirstOrDefault(i => i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); + + if (season != null) + { + return season.Path; + } + + var path = series.Path; + + if (series.ContainsEpisodesWithoutSeasonFolders) + { + return path; + } + + if (seasonNumber == 0) + { + return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName)); + } + + var seasonFolderName = options.SeasonFolderPattern + .Replace("%s", seasonNumber.ToString(_usCulture)) + .Replace("%0s", seasonNumber.ToString("00", _usCulture)) + .Replace("%00s", seasonNumber.ToString("000", _usCulture)); + + return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); + } + + private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) + { + seriesName = _fileSystem.GetValidFilename(seriesName); + episodeTitle = _fileSystem.GetValidFilename(episodeTitle); + + var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); + + var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; + + var result = pattern.Replace("%sn", seriesName) + .Replace("%s.n", seriesName.Replace(" ", ".")) + .Replace("%s_n", seriesName.Replace(" ", "_")) + .Replace("%s", seasonNumber.ToString(_usCulture)) + .Replace("%0s", seasonNumber.ToString("00", _usCulture)) + .Replace("%00s", seasonNumber.ToString("000", _usCulture)) + .Replace("%ext", sourceExtension) + .Replace("%en", episodeTitle) + .Replace("%e.n", episodeTitle.Replace(" ", ".")) + .Replace("%e_n", episodeTitle.Replace(" ", "_")); + + if (endingEpisodeNumber.HasValue) + { + result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture)) + .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture)) + .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture)); + } + + return result.Replace("%e", episodeNumber.ToString(_usCulture)) + .Replace("%0e", episodeNumber.ToString("00", _usCulture)) + .Replace("%00e", episodeNumber.ToString("000", _usCulture)); + } + } +} diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs index 277b26415f..bbd0f74e5b 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -1,5 +1,7 @@ using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -21,14 +23,18 @@ namespace MediaBrowser.Server.Implementations.FileOrganization private readonly ILogger _logger; private readonly IDirectoryWatchers _directoryWatchers; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; - public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager) + public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem) { _taskManager = taskManager; _repo = repo; _logger = logger; _directoryWatchers = directoryWatchers; _libraryManager = libraryManager; + _config = config; + _fileSystem = fileSystem; } public void BeginProcessNewFiles() @@ -53,6 +59,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return _repo.GetResults(query); } + public FileOrganizationResult GetResult(string id) + { + return _repo.GetResult(id); + } + public Task DeleteOriginalFile(string resultId) { var result = _repo.GetResult(resultId); @@ -79,44 +90,27 @@ namespace MediaBrowser.Server.Implementations.FileOrganization throw new ArgumentException("No target path available."); } - _logger.Info("Moving {0} to {1}", result.OriginalPath, result.TargetPath); + var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, + _directoryWatchers); - _directoryWatchers.TemporarilyIgnore(result.TargetPath); - - var copy = File.Exists(result.TargetPath); + await organizer.OrganizeEpisodeFile(result.OriginalPath, _config.Configuration.TvFileOrganizationOptions, true) + .ConfigureAwait(false); - try - { - if (copy) - { - File.Copy(result.OriginalPath, result.TargetPath, true); - } - else - { - File.Move(result.OriginalPath, result.TargetPath); - } - } - finally - { - _directoryWatchers.RemoveTempIgnore(result.TargetPath); - } + await _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None) + .ConfigureAwait(false); + } - if (copy) - { - try - { - File.Delete(result.OriginalPath); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); - } - } + public Task ClearLog() + { + return _repo.DeleteAll(); + } - result.Status = FileSortingStatus.Success; - result.StatusMessage = string.Empty; + public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request) + { + var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, + _directoryWatchers); - await SaveResult(result, CancellationToken.None).ConfigureAwait(false); + await organizer.OrganizeWithCorrection(request, _config.Configuration.TvFileOrganizationOptions).ConfigureAwait(false); await _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None) .ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs b/MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs new file mode 100644 index 0000000000..75e5d92c38 --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs @@ -0,0 +1,92 @@ +using MediaBrowser.Controller.Entities; +using System; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public static class NameUtils + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + internal static Tuple GetMatchScore(string sortedName, int? year, T series) + where T : BaseItem + { + var score = 0; + + var seriesNameWithoutYear = series.Name; + if (series.ProductionYear.HasValue) + { + seriesNameWithoutYear = seriesNameWithoutYear.Replace(series.ProductionYear.Value.ToString(UsCulture), String.Empty); + } + + if (IsNameMatch(sortedName, seriesNameWithoutYear)) + { + score++; + + if (year.HasValue && series.ProductionYear.HasValue) + { + if (year.Value == series.ProductionYear.Value) + { + score++; + } + else + { + // Regardless of name, return a 0 score if the years don't match + return new Tuple(series, 0); + } + } + } + + return new Tuple(series, score); + } + + + private static bool IsNameMatch(string name1, string name2) + { + name1 = GetComparableName(name1); + name2 = GetComparableName(name2); + + return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); + } + + private static string GetComparableName(string name) + { + // TODO: Improve this - should ignore spaces, periods, underscores, most likely all symbols and + // possibly remove sorting words like "the", "and", etc. + + name = RemoveDiacritics(name); + + name = " " + name.ToLower() + " "; + + name = name.Replace(".", " ") + .Replace("_", " ") + .Replace("&", " ") + .Replace("!", " ") + .Replace("(", " ") + .Replace(")", " ") + .Replace(",", " ") + .Replace("-", " ") + .Replace(" a ", String.Empty) + .Replace(" the ", String.Empty) + .Replace(" ", String.Empty); + + return name.Trim(); + } + + /// + /// Removes the diacritics. + /// + /// The text. + /// System.String. + private static string RemoveDiacritics(string text) + { + return String.Concat( + text.Normalize(NormalizationForm.FormD) + .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != + UnicodeCategory.NonSpacingMark) + ).Normalize(NormalizationForm.FormC); + } + } +} diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index 35a3ba0873..340038e4b0 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -14,21 +14,21 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask { - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; + private readonly IDirectoryWatchers _directoryWatchers; private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; private readonly IFileSystem _fileSystem; - private readonly IFileOrganizationService _iFileSortingRepository; - private readonly IDirectoryWatchers _directoryWatchers; + private readonly IServerConfigurationManager _config; + private readonly IFileOrganizationService _organizationService; - public OrganizerScheduledTask(IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IFileOrganizationService iFileSortingRepository, IDirectoryWatchers directoryWatchers) + public OrganizerScheduledTask(IDirectoryWatchers directoryWatchers, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService) { - _config = config; - _logger = logger; + _directoryWatchers = directoryWatchers; _libraryManager = libraryManager; + _logger = logger; _fileSystem = fileSystem; - _iFileSortingRepository = iFileSortingRepository; - _directoryWatchers = directoryWatchers; + _config = config; + _organizationService = organizationService; } public string Name @@ -48,7 +48,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization public Task Execute(CancellationToken cancellationToken, IProgress progress) { - return new TvFileSorter(_libraryManager, _logger, _fileSystem, _iFileSortingRepository, _directoryWatchers).Sort(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress); + return new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _directoryWatchers, _organizationService, _config) + .Organize(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress); } public IEnumerable GetDefaultTriggers() diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs deleted file mode 100644 index 3e653fae41..0000000000 --- a/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs +++ /dev/null @@ -1,563 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.FileOrganization; -using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.FileOrganization; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.FileOrganization -{ - public class TvFileSorter - { - private readonly IDirectoryWatchers _directoryWatchers; - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IFileOrganizationService _iFileSortingRepository; - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - public TvFileSorter(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IFileOrganizationService iFileSortingRepository, IDirectoryWatchers directoryWatchers) - { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - _iFileSortingRepository = iFileSortingRepository; - _directoryWatchers = directoryWatchers; - } - - public async Task Sort(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress progress) - { - var minFileBytes = options.MinFileSizeMb * 1024 * 1024; - - var watchLocations = options.WatchLocations.ToList(); - - var eligibleFiles = watchLocations.SelectMany(GetFilesToSort) - .OrderBy(_fileSystem.GetCreationTimeUtc) - .Where(i => EntityResolutionHelper.IsVideoFile(i.FullName) && i.Length >= minFileBytes) - .ToList(); - - progress.Report(10); - - var scanLibrary = false; - - if (eligibleFiles.Count > 0) - { - var allSeries = _libraryManager.RootFolder - .RecursiveChildren.OfType() - .Where(i => i.LocationType == LocationType.FileSystem) - .ToList(); - - var numComplete = 0; - - foreach (var file in eligibleFiles) - { - var result = await SortFile(file.FullName, options, allSeries).ConfigureAwait(false); - - if (result.Status == FileSortingStatus.Success) - { - scanLibrary = true; - } - - numComplete++; - double percent = numComplete; - percent /= eligibleFiles.Count; - - progress.Report(10 + (89 * percent)); - } - } - - cancellationToken.ThrowIfCancellationRequested(); - progress.Report(99); - - foreach (var path in watchLocations) - { - if (options.LeftOverFileExtensionsToDelete.Length > 0) - { - DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete); - } - - if (options.DeleteEmptyFolders) - { - DeleteEmptyFolders(path); - } - } - - if (scanLibrary) - { - await _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None) - .ConfigureAwait(false); - } - - progress.Report(100); - } - - /// - /// Gets the eligible files. - /// - /// The path. - /// IEnumerable{FileInfo}. - private IEnumerable GetFilesToSort(string path) - { - try - { - return new DirectoryInfo(path) - .EnumerateFiles("*", SearchOption.AllDirectories) - .ToList(); - } - catch (IOException ex) - { - _logger.ErrorException("Error getting files from {0}", ex, path); - - return new List(); - } - } - - /// - /// Sorts the file. - /// - /// The path. - /// The options. - /// All series. - private async Task SortFile(string path, TvFileOrganizationOptions options, IEnumerable allSeries) - { - _logger.Info("Sorting file {0}", path); - - var result = new FileOrganizationResult - { - Date = DateTime.UtcNow, - OriginalPath = path, - OriginalFileName = Path.GetFileName(path), - Type = FileOrganizerType.Episode - }; - - var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path); - - if (!string.IsNullOrEmpty(seriesName)) - { - var season = TVUtils.GetSeasonNumberFromEpisodeFile(path); - - result.ExtractedSeasonNumber = season; - - if (season.HasValue) - { - // Passing in true will include a few extra regex's - var episode = TVUtils.GetEpisodeNumberFromFile(path, true); - - result.ExtractedEpisodeNumber = episode; - - if (episode.HasValue) - { - _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode); - - var endingEpisodeNumber = TVUtils.GetEndingEpisodeNumberFromFile(path); - - result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; - - SortFile(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, allSeries, result); - } - else - { - var msg = string.Format("Unable to determine episode number from {0}", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - } - } - else - { - var msg = string.Format("Unable to determine season number from {0}", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - } - } - else - { - var msg = string.Format("Unable to determine series name from {0}", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - } - - await LogResult(result).ConfigureAwait(false); - - return result; - } - - /// - /// Sorts the file. - /// - /// The path. - /// Name of the series. - /// The season number. - /// The episode number. - /// The ending epiosde number. - /// The options. - /// All series. - /// The result. - private void SortFile(string path, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, IEnumerable allSeries, FileOrganizationResult result) - { - var series = GetMatchingSeries(seriesName, allSeries, result); - - if (series == null) - { - var msg = string.Format("Unable to find series in library matching name {0}", seriesName); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - return; - } - - _logger.Info("Sorting file {0} into series {1}", path, series.Path); - - // Proceed to sort the file - var newPath = GetNewPath(path, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options); - - if (string.IsNullOrEmpty(newPath)) - { - var msg = string.Format("Unable to sort {0} because target path could not be determined.", path); - result.Status = FileSortingStatus.Failure; - result.StatusMessage = msg; - _logger.Warn(msg); - return; - } - - _logger.Info("Sorting file {0} to new path {1}", path, newPath); - result.TargetPath = newPath; - - var targetExists = File.Exists(result.TargetPath); - if (!options.OverwriteExistingEpisodes && targetExists) - { - result.Status = FileSortingStatus.SkippedExisting; - return; - } - - PerformFileSorting(options, result, targetExists); - } - - /// - /// Performs the file sorting. - /// - /// The options. - /// The result. - /// if set to true [copy]. - private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result, bool copy) - { - _directoryWatchers.TemporarilyIgnore(result.TargetPath); - - try - { - if (copy) - { - File.Copy(result.OriginalPath, result.TargetPath, true); - } - else - { - File.Move(result.OriginalPath, result.TargetPath); - } - } - catch (Exception ex) - { - var errorMsg = string.Format("Failed to move file from {0} to {1}", result.OriginalPath, result.TargetPath); - - result.Status = FileSortingStatus.Failure; - result.StatusMessage = errorMsg; - _logger.ErrorException(errorMsg, ex); - - return; - } - finally - { - _directoryWatchers.RemoveTempIgnore(result.TargetPath); - } - - if (copy) - { - try - { - File.Delete(result.OriginalPath); - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); - } - } - } - - /// - /// Logs the result. - /// - /// The result. - /// Task. - private Task LogResult(FileOrganizationResult result) - { - return _iFileSortingRepository.SaveResult(result, CancellationToken.None); - } - - /// - /// Gets the new path. - /// - /// The source path. - /// The series. - /// The season number. - /// The episode number. - /// The ending episode number. - /// The options. - /// System.String. - private string GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options) - { - // If season and episode numbers match - var currentEpisodes = series.RecursiveChildren.OfType() - .Where(i => i.IndexNumber.HasValue && - i.IndexNumber.Value == episodeNumber && - i.ParentIndexNumber.HasValue && - i.ParentIndexNumber.Value == seasonNumber) - .ToList(); - - if (currentEpisodes.Count == 0) - { - return null; - } - - var newPath = GetSeasonFolderPath(series, seasonNumber, options); - - var episode = currentEpisodes.First(); - - var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options); - - newPath = Path.Combine(newPath, episodeFileName); - - return newPath; - } - - private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) - { - seriesName = _fileSystem.GetValidFilename(seriesName); - episodeTitle = _fileSystem.GetValidFilename(episodeTitle); - - var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); - - var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; - - var result = pattern.Replace("%sn", seriesName) - .Replace("%s.n", seriesName.Replace(" ", ".")) - .Replace("%s_n", seriesName.Replace(" ", "_")) - .Replace("%s", seasonNumber.ToString(UsCulture)) - .Replace("%0s", seasonNumber.ToString("00", UsCulture)) - .Replace("%00s", seasonNumber.ToString("000", UsCulture)) - .Replace("%ext", sourceExtension) - .Replace("%en", episodeTitle) - .Replace("%e.n", episodeTitle.Replace(" ", ".")) - .Replace("%e_n", episodeTitle.Replace(" ", "_")); - - if (endingEpisodeNumber.HasValue) - { - result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(UsCulture)) - .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", UsCulture)) - .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", UsCulture)); - } - - return result.Replace("%e", episodeNumber.ToString(UsCulture)) - .Replace("%0e", episodeNumber.ToString("00", UsCulture)) - .Replace("%00e", episodeNumber.ToString("000", UsCulture)); - } - - /// - /// Gets the season folder path. - /// - /// The series. - /// The season number. - /// The options. - /// System.String. - private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) - { - // If there's already a season folder, use that - var season = series - .RecursiveChildren - .OfType() - .FirstOrDefault(i => i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber); - - if (season != null) - { - return season.Path; - } - - var path = series.Path; - - if (series.ContainsEpisodesWithoutSeasonFolders) - { - return path; - } - - if (seasonNumber == 0) - { - return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName)); - } - - var seasonFolderName = options.SeasonFolderPattern - .Replace("%s", seasonNumber.ToString(UsCulture)) - .Replace("%0s", seasonNumber.ToString("00", UsCulture)) - .Replace("%00s", seasonNumber.ToString("000", UsCulture)); - - return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); - } - - /// - /// Gets the matching series. - /// - /// Name of the series. - /// All series. - /// Series. - private Series GetMatchingSeries(string seriesName, IEnumerable allSeries, FileOrganizationResult result) - { - int? yearInName; - var nameWithoutYear = seriesName; - NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out yearInName); - - result.ExtractedName = nameWithoutYear; - result.ExtractedYear = yearInName; - - return allSeries.Select(i => GetMatchScore(nameWithoutYear, yearInName, i)) - .Where(i => i.Item2 > 0) - .OrderByDescending(i => i.Item2) - .Select(i => i.Item1) - .FirstOrDefault(); - } - - private Tuple GetMatchScore(string sortedName, int? year, Series series) - { - var score = 0; - - if (IsNameMatch(sortedName, series.Name)) - { - score++; - - if (year.HasValue && series.ProductionYear.HasValue) - { - if (year.Value == series.ProductionYear.Value) - { - score++; - } - else - { - // Regardless of name, return a 0 score if the years don't match - return new Tuple(series, 0); - } - } - } - - return new Tuple(series, score); - } - - private bool IsNameMatch(string name1, string name2) - { - name1 = GetComparableName(name1); - name2 = GetComparableName(name2); - - return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); - } - - private string GetComparableName(string name) - { - // TODO: Improve this - should ignore spaces, periods, underscores, most likely all symbols and - // possibly remove sorting words like "the", "and", etc. - - name = RemoveDiacritics(name); - - name = " " + name.ToLower() + " "; - - name = name.Replace(".", " ") - .Replace("_", " ") - .Replace("&", " ") - .Replace("!", " ") - .Replace(",", " ") - .Replace("-", " ") - .Replace(" a ", string.Empty) - .Replace(" the ", string.Empty) - .Replace(" ", string.Empty); - - return name.Trim(); - } - - /// - /// Removes the diacritics. - /// - /// The text. - /// System.String. - private string RemoveDiacritics(string text) - { - return string.Concat( - text.Normalize(NormalizationForm.FormD) - .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != - UnicodeCategory.NonSpacingMark) - ).Normalize(NormalizationForm.FormC); - } - - /// - /// Deletes the left over files. - /// - /// The path. - /// The extensions. - private void DeleteLeftOverFiles(string path, IEnumerable extensions) - { - var eligibleFiles = new DirectoryInfo(path) - .EnumerateFiles("*", SearchOption.AllDirectories) - .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) - .ToList(); - - foreach (var file in eligibleFiles) - { - try - { - File.Delete(file.FullName); - } - catch (IOException ex) - { - _logger.ErrorException("Error deleting file {0}", ex, file.FullName); - } - } - } - - /// - /// Deletes the empty folders. - /// - /// The path. - private void DeleteEmptyFolders(string path) - { - try - { - foreach (var d in Directory.EnumerateDirectories(path)) - { - DeleteEmptyFolders(d); - } - - var entries = Directory.EnumerateFileSystemEntries(path); - - if (!entries.Any()) - { - try - { - Directory.Delete(path); - } - catch (UnauthorizedAccessException) { } - catch (DirectoryNotFoundException) { } - } - } - catch (UnauthorizedAccessException) { } - } - } -} diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs new file mode 100644 index 0000000000..ad3208b771 --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -0,0 +1,176 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public class TvFolderOrganizer + { + private readonly IDirectoryWatchers _directoryWatchers; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IFileOrganizationService _organizationService; + private readonly IServerConfigurationManager _config; + + public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IDirectoryWatchers directoryWatchers, IFileOrganizationService organizationService, IServerConfigurationManager config) + { + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + _directoryWatchers = directoryWatchers; + _organizationService = organizationService; + _config = config; + } + + public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress progress) + { + var minFileBytes = options.MinFileSizeMb * 1024 * 1024; + + var watchLocations = options.WatchLocations.ToList(); + + var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) + .OrderBy(_fileSystem.GetCreationTimeUtc) + .Where(i => EntityResolutionHelper.IsVideoFile(i.FullName) && i.Length >= minFileBytes) + .ToList(); + + progress.Report(10); + + var scanLibrary = false; + + if (eligibleFiles.Count > 0) + { + var numComplete = 0; + + foreach (var file in eligibleFiles) + { + var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, + _directoryWatchers); + + var result = await organizer.OrganizeEpisodeFile(file.FullName, options, false).ConfigureAwait(false); + + if (result.Status == FileSortingStatus.Success) + { + scanLibrary = true; + } + + numComplete++; + double percent = numComplete; + percent /= eligibleFiles.Count; + + progress.Report(10 + (89 * percent)); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + progress.Report(99); + + foreach (var path in watchLocations) + { + if (options.LeftOverFileExtensionsToDelete.Length > 0) + { + DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete); + } + + if (options.DeleteEmptyFolders) + { + DeleteEmptyFolders(path); + } + } + + if (scanLibrary) + { + await _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None) + .ConfigureAwait(false); + } + + progress.Report(100); + } + + /// + /// Gets the files to organize. + /// + /// The path. + /// IEnumerable{FileInfo}. + private IEnumerable GetFilesToOrganize(string path) + { + try + { + return new DirectoryInfo(path) + .EnumerateFiles("*", SearchOption.AllDirectories) + .ToList(); + } + catch (IOException ex) + { + _logger.ErrorException("Error getting files from {0}", ex, path); + + return new List(); + } + } + + /// + /// Deletes the left over files. + /// + /// The path. + /// The extensions. + private void DeleteLeftOverFiles(string path, IEnumerable extensions) + { + var eligibleFiles = new DirectoryInfo(path) + .EnumerateFiles("*", SearchOption.AllDirectories) + .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + foreach (var file in eligibleFiles) + { + try + { + File.Delete(file.FullName); + } + catch (IOException ex) + { + _logger.ErrorException("Error deleting file {0}", ex, file.FullName); + } + } + } + + /// + /// Deletes the empty folders. + /// + /// The path. + private void DeleteEmptyFolders(string path) + { + try + { + foreach (var d in Directory.EnumerateDirectories(path)) + { + DeleteEmptyFolders(d); + } + + var entries = Directory.EnumerateFileSystemEntries(path); + + if (!entries.Any()) + { + try + { + Directory.Delete(path); + } + catch (UnauthorizedAccessException) { } + catch (DirectoryNotFoundException) { } + } + } + catch (UnauthorizedAccessException) { } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 420c9f583f..d8af929919 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -19,20 +19,23 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV protected override Episode Resolve(ItemResolveArgs args) { var parent = args.Parent; + + if (parent == null) + { + return null; + } + var season = parent as Season; // Just in case the user decided to nest episodes. // Not officially supported but in some cases we can handle it. if (season == null) { - if (parent != null) - { - season = parent.Parents.OfType().FirstOrDefault(); - } + season = parent.Parents.OfType().FirstOrDefault(); } // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something - if (season != null || args.Parent is Series) + if (season != null || parent.Parents.OfType().Any()) { Episode episode = null; diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 349d93d831..314e7a4582 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -117,8 +117,10 @@ + - + + diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs index eecb6aae42..e435b1644c 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/MediaEncoder.cs @@ -956,7 +956,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder /// The process. /// The timeout. /// true if XXXX, false otherwise - private bool StartAndWaitForProcess(Process process, int timeout = 10000) + private bool StartAndWaitForProcess(Process process, int timeout = 12000) { process.Start(); diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs index e20139e086..4463ac6f30 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _saveResultCommand; private IDbCommand _deleteResultCommand; + private IDbCommand _deleteAllCommand; public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths) { @@ -85,6 +86,9 @@ namespace MediaBrowser.Server.Implementations.Persistence _deleteResultCommand.CommandText = "delete from organizationresults where ResultId = @ResultId"; _deleteResultCommand.Parameters.Add(_saveResultCommand, "@ResultId"); + + _deleteAllCommand = _connection.CreateCommand(); + _deleteAllCommand.CommandText = "delete from organizationresults"; } public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) @@ -188,7 +192,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } catch (Exception e) { - _logger.ErrorException("Failed to save FileOrganizationResult:", e); + _logger.ErrorException("Failed to delete FileOrganizationResult:", e); if (transaction != null) { @@ -208,6 +212,53 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public async Task DeleteAll() + { + await _writeLock.WaitAsync().ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + _deleteAllCommand.Transaction = transaction; + + _deleteAllCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + _logger.ErrorException("Failed to delete results", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + _writeLock.Release(); + } + } + public QueryResult GetResults(FileOrganizationResultQuery query) { if (query == null) diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index ef77da8b84..045c9f18c6 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -294,7 +294,7 @@ namespace MediaBrowser.ServerApplication var newsService = new Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer); RegisterSingleInstance(newsService); - var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, Logger, DirectoryWatchers, LibraryManager); + var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, Logger, DirectoryWatchers, LibraryManager, ServerConfigurationManager, FileSystemManager); RegisterSingleInstance(fileOrganizationService); progress.Report(15); diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index cf7eb989bd..bfee13bf82 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -134,12 +134,13 @@ ..\ThirdParty\ServiceStack\ServiceStack.Interfaces.dll - + False - ..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.dll + ..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.dll - - ..\packages\SimpleInjector.2.4.0\lib\net45\SimpleInjector.Diagnostics.dll + + False + ..\packages\SimpleInjector.2.4.1\lib\net45\SimpleInjector.Diagnostics.dll diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config index 740cfe5f3a..d83a85d090 100644 --- a/MediaBrowser.ServerApplication/packages.config +++ b/MediaBrowser.ServerApplication/packages.config @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/MediaBrowser.WebDashboard/ApiClient.js b/MediaBrowser.WebDashboard/ApiClient.js index 49ce5fd959..7a90b12c9b 100644 --- a/MediaBrowser.WebDashboard/ApiClient.js +++ b/MediaBrowser.WebDashboard/ApiClient.js @@ -687,6 +687,16 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; + self.clearOrganizationLog = function () { + + var url = self.getUrl("Library/FileOrganizations"); + + return self.ajax({ + type: "DELETE", + url: url + }); + }; + self.performOrganization = function (id) { var url = self.getUrl("Library/FileOrganizations/" + id + "/Organize"); @@ -697,6 +707,16 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi }); }; + self.performEpisodeOrganization = function (id, options) { + + var url = self.getUrl("Library/FileOrganizations/" + id + "/Episode/Organize", options || {}); + + return self.ajax({ + type: "POST", + url: url + }); + }; + self.getLiveTvSeriesTimer = function (id) { if (!id) { @@ -2984,7 +3004,14 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout, wi throw new Error("null userId"); } - var url = self.getUrl("Users/" + userId + "/Items", options); + var url; + + if ((typeof userId).toString().toLowerCase() == 'string') { + url = self.getUrl("Users/" + userId + "/Items", options); + } else { + options = userId; + url = self.getUrl("Items", options || {}); + } return self.ajax({ type: "GET", diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 3f77d9541e..d1427603d5 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file