From c798529caca49ef8c323c0e003dd9f4ba0394b5a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 20 Jan 2014 11:09:53 -0500 Subject: #680 - Support new episode file sorting --- .../FileOrganization/FileOrganizationService.cs | 38 ++ .../FileOrganization/OrganizerScheduledTask.cs | 69 +++ .../FileOrganization/TvFileSorter.cs | 482 +++++++++++++++++++++ .../FileSorting/SortingScheduledTask.cs | 69 --- .../FileSorting/TvFileSorter.cs | 464 -------------------- .../MediaBrowser.Server.Implementations.csproj | 7 +- .../SqliteFileOrganizationRepository.cs | 118 +++++ .../Persistence/SqliteFileSortingRepository.cs | 21 - 8 files changed, 711 insertions(+), 557 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs create mode 100644 MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs delete mode 100644 MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs delete mode 100644 MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs create mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs delete mode 100644 MediaBrowser.Server.Implementations/Persistence/SqliteFileSortingRepository.cs (limited to 'MediaBrowser.Server.Implementations') diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs new file mode 100644 index 0000000000..9d8236bde1 --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -0,0 +1,38 @@ +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.FileOrganization; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public class FileOrganizationService : IFileOrganizationService + { + private readonly ITaskManager _taskManager; + private readonly IFileOrganizationRepository _repo; + + public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo) + { + _taskManager = taskManager; + _repo = repo; + } + + public void BeginProcessNewFiles() + { + _taskManager.CancelIfRunningAndQueue(); + } + + + public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) + { + return _repo.SaveResult(result, cancellationToken); + } + + public IEnumerable GetResults(FileOrganizationResultQuery query) + { + return _repo.GetResults(query); + } + } +} diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs new file mode 100644 index 0000000000..5c5a83cb6d --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -0,0 +1,69 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask + { + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _fileSystem; + private readonly IFileOrganizationService _iFileSortingRepository; + + public OrganizerScheduledTask(IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IFileOrganizationService iFileSortingRepository) + { + _config = config; + _logger = logger; + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _iFileSortingRepository = iFileSortingRepository; + } + + public string Name + { + get { return "Organize new media files"; } + } + + public string Description + { + get { return "Processes new files available in the configured watch folder."; } + } + + public string Category + { + get { return "Library"; } + } + + public Task Execute(CancellationToken cancellationToken, IProgress progress) + { + return new TvFileSorter(_libraryManager, _logger, _fileSystem, _iFileSortingRepository).Sort(_config.Configuration.TvFileOrganizationOptions, cancellationToken, progress); + } + + public IEnumerable GetDefaultTriggers() + { + return new ITaskTrigger[] + { + new IntervalTrigger{ Interval = TimeSpan.FromMinutes(5)} + }; + } + + public bool IsHidden + { + get { return !_config.Configuration.TvFileOrganizationOptions.IsEnabled; } + } + + public bool IsEnabled + { + get { return _config.Configuration.TvFileOrganizationOptions.IsEnabled; } + } + } +} diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs b/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs new file mode 100644 index 0000000000..24e2c094b8 --- /dev/null +++ b/MediaBrowser.Server.Implementations/FileOrganization/TvFileSorter.cs @@ -0,0 +1,482 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.FileOrganization; +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.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.FileOrganization +{ + public class TvFileSorter + { + 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) + { + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + _iFileSortingRepository = iFileSortingRepository; + } + + 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); + + 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) + { + await SortFile(file.FullName, options, allSeries).ConfigureAwait(false); + + numComplete++; + double percent = numComplete; + percent /= eligibleFiles.Count; + + progress.Report(10 + (89 * percent)); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + progress.Report(99); + + if (!options.EnableTrialMode) + { + foreach (var path in watchLocations) + { + if (options.LeftOverFileExtensionsToDelete.Length > 0) + { + DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete); + } + + if (options.DeleteEmptyFolders) + { + DeleteEmptyFolders(path); + } + } + } + + 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 Task SortFile(string path, TvFileOrganizationOptions options, IEnumerable allSeries) + { + _logger.Info("Sorting file {0}", path); + + var result = new FileOrganizationResult + { + Date = DateTime.UtcNow, + OriginalPath = path + }; + + var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path); + + if (!string.IsNullOrEmpty(seriesName)) + { + var season = TVUtils.GetSeasonNumberFromEpisodeFile(path); + + if (season.HasValue) + { + // Passing in true will include a few extra regex's + var episode = TVUtils.GetEpisodeNumberFromFile(path, true); + + if (episode.HasValue) + { + _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode); + + SortFile(path, seriesName, season.Value, episode.Value, options, allSeries, result); + } + else + { + var msg = string.Format("Unable to determine episode number from {0}", path); + result.Status = FileSortingStatus.Failure; + result.ErrorMessage = msg; + _logger.Warn(msg); + } + } + else + { + var msg = string.Format("Unable to determine season number from {0}", path); + result.Status = FileSortingStatus.Failure; + result.ErrorMessage = msg; + _logger.Warn(msg); + } + } + else + { + var msg = string.Format("Unable to determine series name from {0}", path); + result.Status = FileSortingStatus.Failure; + result.ErrorMessage = msg; + _logger.Warn(msg); + } + + return LogResult(result); + } + + /// + /// Sorts the file. + /// + /// The path. + /// Name of the series. + /// The season number. + /// The episode number. + /// The options. + /// All series. + /// The result. + private void SortFile(string path, string seriesName, int seasonNumber, int episodeNumber, TvFileOrganizationOptions options, IEnumerable allSeries, FileOrganizationResult result) + { + var series = GetMatchingSeries(seriesName, allSeries); + + if (series == null) + { + var msg = string.Format("Unable to find series in library matching name {0}", seriesName); + result.Status = FileSortingStatus.Failure; + result.ErrorMessage = 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, 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.ErrorMessage = msg; + _logger.Warn(msg); + return; + } + + _logger.Info("Sorting file {0} to new path {1}", path, newPath); + result.TargetPath = newPath; + + if (options.EnableTrialMode) + { + result.Status = FileSortingStatus.SkippedTrial; + return; + } + + 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) + { + 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.ErrorMessage = errorMsg; + _logger.ErrorException(errorMsg, ex); + return; + } + + 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 options. + /// System.String. + private string GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, TvFileOrganizationOptions options) + { + 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 = currentEpisodes + .Where(i => i.LocationType == LocationType.FileSystem) + .Select(i => i.Path) + .FirstOrDefault(); + + if (string.IsNullOrEmpty(newPath)) + { + newPath = GetSeasonFolderPath(series, seasonNumber, options); + + var episode = currentEpisodes.First(); + + var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, episode.Name, options); + + newPath = Path.Combine(newPath, episodeFileName); + } + + return newPath; + } + + private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, string episodeTitle, TvFileOrganizationOptions options) + { + seriesName = _fileSystem.GetValidFilename(seriesName); + episodeTitle = _fileSystem.GetValidFilename(episodeTitle); + + var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); + + return options.EpisodeNamePattern.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(" ", "_")) + .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) + { + int? yearInName; + var nameWithoutYear = seriesName; + NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out 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; + + // TODO: Improve this - should ignore spaces, periods, underscores, most likely all symbols and + // possibly remove sorting words like "the", "and", etc. + if (string.Equals(sortedName, series.Name, StringComparison.OrdinalIgnoreCase)) + { + 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); + } + + /// + /// 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/FileSorting/SortingScheduledTask.cs b/MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs deleted file mode 100644 index 85d172d361..0000000000 --- a/MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs +++ /dev/null @@ -1,69 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.FileSorting -{ - public class SortingScheduledTask : IScheduledTask, IConfigurableScheduledTask - { - private readonly IServerConfigurationManager _config; - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly IFileSystem _fileSystem; - private readonly IFileSortingRepository _iFileSortingRepository; - - public SortingScheduledTask(IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem, IFileSortingRepository iFileSortingRepository) - { - _config = config; - _logger = logger; - _libraryManager = libraryManager; - _fileSystem = fileSystem; - _iFileSortingRepository = iFileSortingRepository; - } - - public string Name - { - get { return "Sort new files"; } - } - - public string Description - { - get { return "Processes new files available in the configured sorting location."; } - } - - public string Category - { - get { return "Library"; } - } - - public Task Execute(CancellationToken cancellationToken, IProgress progress) - { - return new TvFileSorter(_libraryManager, _logger, _fileSystem, _iFileSortingRepository).Sort(_config.Configuration.TvFileSortingOptions, cancellationToken, progress); - } - - public IEnumerable GetDefaultTriggers() - { - return new ITaskTrigger[] - { - new IntervalTrigger{ Interval = TimeSpan.FromMinutes(5)} - }; - } - - public bool IsHidden - { - get { return !_config.Configuration.TvFileSortingOptions.IsEnabled; } - } - - public bool IsEnabled - { - get { return _config.Configuration.TvFileSortingOptions.IsEnabled; } - } - } -} diff --git a/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs b/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs deleted file mode 100644 index 093a1dba66..0000000000 --- a/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs +++ /dev/null @@ -1,464 +0,0 @@ -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.FileSorting; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.FileSorting -{ - public class TvFileSorter - { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; - private readonly IFileSortingRepository _iFileSortingRepository; - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - public TvFileSorter(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IFileSortingRepository iFileSortingRepository) - { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - _iFileSortingRepository = iFileSortingRepository; - } - - public async Task Sort(TvFileSortingOptions 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); - - 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) - { - await SortFile(file.FullName, options, allSeries).ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= eligibleFiles.Count; - - progress.Report(10 + (89 * percent)); - } - } - - cancellationToken.ThrowIfCancellationRequested(); - progress.Report(99); - - if (!options.EnableTrialMode) - { - foreach (var path in watchLocations) - { - if (options.LeftOverFileExtensionsToDelete.Length > 0) - { - DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete); - } - - if (options.DeleteEmptyFolders) - { - DeleteEmptyFolders(path); - } - } - } - - 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 Task SortFile(string path, TvFileSortingOptions options, IEnumerable allSeries) - { - _logger.Info("Sorting file {0}", path); - - var result = new FileSortingResult - { - Date = DateTime.UtcNow, - OriginalPath = path - }; - - var seriesName = TVUtils.GetSeriesNameFromEpisodeFile(path); - - if (!string.IsNullOrEmpty(seriesName)) - { - var season = TVUtils.GetSeasonNumberFromEpisodeFile(path); - - if (season.HasValue) - { - // Passing in true will include a few extra regex's - var episode = TVUtils.GetEpisodeNumberFromFile(path, true); - - if (episode.HasValue) - { - _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, season, episode); - - SortFile(path, seriesName, season.Value, episode.Value, options, allSeries, result); - } - else - { - var msg = string.Format("Unable to determine episode number from {0}", path); - result.Status = FileSortingStatus.Failure; - result.ErrorMessage = msg; - _logger.Warn(msg); - } - } - else - { - var msg = string.Format("Unable to determine season number from {0}", path); - result.Status = FileSortingStatus.Failure; - result.ErrorMessage = msg; - _logger.Warn(msg); - } - } - else - { - var msg = string.Format("Unable to determine series name from {0}", path); - result.Status = FileSortingStatus.Failure; - result.ErrorMessage = msg; - _logger.Warn(msg); - } - - return LogResult(result); - } - - /// - /// Sorts the file. - /// - /// The path. - /// Name of the series. - /// The season number. - /// The episode number. - /// The options. - /// All series. - /// The result. - private void SortFile(string path, string seriesName, int seasonNumber, int episodeNumber, TvFileSortingOptions options, IEnumerable allSeries, FileSortingResult result) - { - var series = GetMatchingSeries(seriesName, allSeries); - - if (series == null) - { - var msg = string.Format("Unable to find series in library matching name {0}", seriesName); - result.Status = FileSortingStatus.Failure; - result.ErrorMessage = msg; - _logger.Warn(msg); - return; - } - - _logger.Info("Sorting file {0} into series {1}", path, series.Path); - - // Proceed to sort the file - var newPath = GetNewPath(series, seasonNumber, episodeNumber, 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.ErrorMessage = msg; - _logger.Warn(msg); - return; - } - - _logger.Info("Sorting file {0} to new path {1}", path, newPath); - result.TargetPath = newPath; - - if (options.EnableTrialMode) - { - result.Status = FileSortingStatus.SkippedTrial; - return; - } - - 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(TvFileSortingOptions options, FileSortingResult result, bool copy) - { - 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.ErrorMessage = errorMsg; - _logger.ErrorException(errorMsg, ex); - return; - } - - 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(FileSortingResult result) - { - return _iFileSortingRepository.SaveResult(result, CancellationToken.None); - } - - /// - /// Gets the new path. - /// - /// The series. - /// The season number. - /// The episode number. - /// The options. - /// System.String. - private string GetNewPath(Series series, int seasonNumber, int episodeNumber, TvFileSortingOptions options) - { - 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 = currentEpisodes - .Where(i => i.LocationType == LocationType.FileSystem) - .Select(i => i.Path) - .FirstOrDefault(); - - if (string.IsNullOrEmpty(newPath)) - { - newPath = GetSeasonFolderPath(series, seasonNumber, options); - - var episode = currentEpisodes.First(); - - var episodeFileName = string.Format("{0} - {1}x{2} - {3}", - - _fileSystem.GetValidFilename(series.Name), - seasonNumber.ToString(UsCulture), - episodeNumber.ToString("00", UsCulture), - _fileSystem.GetValidFilename(episode.Name) - ); - - 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, TvFileSortingOptions 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) - { - int? yearInName; - var nameWithoutYear = seriesName; - NameParser.ParseName(nameWithoutYear, out nameWithoutYear, out 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; - - // TODO: Improve this - if (string.Equals(sortedName, series.Name, StringComparison.OrdinalIgnoreCase)) - { - 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); - } - - /// - /// 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/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 95bafd1a30..349d93d831 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -117,11 +117,12 @@ - + + - + @@ -179,7 +180,7 @@ - + diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs new file mode 100644 index 0000000000..a95f84f067 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs @@ -0,0 +1,118 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + public class SqliteFileOrganizationRepository : IFileOrganizationRepository + { + private IDbConnection _connection; + + private readonly ILogger _logger; + + private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); + private SqliteShrinkMemoryTimer _shrinkMemoryTimer; + private readonly IServerApplicationPaths _appPaths; + + public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths) + { + _appPaths = appPaths; + + _logger = logManager.GetLogger(GetType().Name); + } + + /// + /// Opens the connection to the database + /// + /// Task. + public async Task Initialize() + { + var dbFile = Path.Combine(_appPaths.DataPath, "fileorganization.db"); + + _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); + + string[] queries = { + + //pragmas + "pragma temp_store = memory", + + "pragma shrink_memory" + }; + + _connection.RunQueries(queries, _logger); + + PrepareStatements(); + + _shrinkMemoryTimer = new SqliteShrinkMemoryTimer(_connection, _writeLock, _logger); + } + + private void PrepareStatements() + { + } + + public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public IEnumerable GetResults(FileOrganizationResultQuery query) + { + return new List(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + try + { + lock (_disposeLock) + { + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } + + if (_connection != null) + { + if (_connection.IsOpen()) + { + _connection.Close(); + } + + _connection.Dispose(); + _connection = null; + } + } + } + catch (Exception ex) + { + _logger.ErrorException("Error disposing database", ex); + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteFileSortingRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteFileSortingRepository.cs deleted file mode 100644 index 9f24d4d9ce..0000000000 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteFileSortingRepository.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.FileSorting; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class SqliteFileSortingRepository : IFileSortingRepository - { - public Task SaveResult(FileSortingResult result, CancellationToken cancellationToken) - { - return Task.FromResult(true); - } - - public IEnumerable GetResults(FileSortingResultQuery query) - { - return new List(); - } - } -} -- cgit v1.2.3