aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations/FileSorting
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Server.Implementations/FileSorting')
-rw-r--r--MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs96
-rw-r--r--MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs186
2 files changed, 282 insertions, 0 deletions
diff --git a/MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs b/MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs
new file mode 100644
index 000000000..201e282c0
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/FileSorting/SortingScheduledTask.cs
@@ -0,0 +1,96 @@
+using MediaBrowser.Common.ScheduledTasks;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+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;
+
+ public SortingScheduledTask(IServerConfigurationManager config, ILogger logger, ILibraryManager libraryManager)
+ {
+ _config = config;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ }
+
+ 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<double> progress)
+ {
+ return Task.Run(() => SortFiles(cancellationToken, progress), cancellationToken);
+ }
+
+ private void SortFiles(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var numComplete = 0;
+
+ var paths = _config.Configuration.FileSortingOptions.TvWatchLocations.ToList();
+
+ foreach (var path in paths)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ SortFiles(path);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error sorting files from {0}", ex, path);
+ }
+
+ numComplete++;
+ double percent = numComplete;
+ percent /= paths.Count;
+
+ progress.Report(100 * percent);
+ }
+ }
+
+ private void SortFiles(string path)
+ {
+ new TvFileSorter(_libraryManager, _logger).Sort(path, _config.Configuration.FileSortingOptions);
+ }
+
+ public IEnumerable<ITaskTrigger> GetDefaultTriggers()
+ {
+ return new ITaskTrigger[]
+ {
+ new IntervalTrigger{ Interval = TimeSpan.FromMinutes(5)}
+ };
+ }
+
+ public bool IsHidden
+ {
+ get { return !_config.Configuration.FileSortingOptions.IsEnabled; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return _config.Configuration.FileSortingOptions.IsEnabled; }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs b/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs
new file mode 100644
index 000000000..e2a967ef3
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/FileSorting/TvFileSorter.cs
@@ -0,0 +1,186 @@
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Server.Implementations.FileSorting
+{
+ public class TvFileSorter
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger _logger;
+
+ public TvFileSorter(ILibraryManager libraryManager, ILogger logger)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ }
+
+ public void Sort(string path, FileSortingOptions options)
+ {
+ var minFileBytes = options.MinFileSizeMb * 1024 * 1024;
+
+ var allSeries = _libraryManager.RootFolder
+ .RecursiveChildren.OfType<Series>()
+ .Where(i => i.LocationType == LocationType.FileSystem)
+ .ToList();
+
+ var eligibleFiles = new DirectoryInfo(path)
+ .EnumerateFiles("*", SearchOption.AllDirectories)
+ .Where(i => EntityResolutionHelper.IsVideoFile(i.FullName) && i.Length >= minFileBytes)
+ .ToList();
+
+ foreach (var file in eligibleFiles)
+ {
+ SortFile(file.FullName, options, allSeries);
+ }
+
+ if (options.LeftOverFileExtensionsToDelete.Length > 0)
+ {
+ DeleteLeftOverFiles(path, options.LeftOverFileExtensionsToDelete);
+ }
+
+ if (options.DeleteEmptyFolders)
+ {
+ DeleteEmptyFolders(path);
+ }
+ }
+
+ private void SortFile(string path, FileSortingOptions options, IEnumerable<Series> allSeries)
+ {
+ _logger.Info("Sorting file {0}", 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);
+ }
+ else
+ {
+ _logger.Warn("Unable to determine episode number from {0}", path);
+ }
+ }
+ else
+ {
+ _logger.Warn("Unable to determine season number from {0}", path);
+ }
+ }
+ else
+ {
+ _logger.Warn("Unable to determine series name from {0}", path);
+ }
+ }
+
+ private void SortFile(string path, string seriesName, int seasonNumber, int episodeNumber, FileSortingOptions options, IEnumerable<Series> allSeries)
+ {
+ var series = GetMatchingSeries(seriesName, allSeries);
+
+ if (series == null)
+ {
+ _logger.Warn("Unable to find series in library matching name {0}", seriesName);
+ return;
+ }
+
+ _logger.Info("Sorting file {0} into series {1}", path, series.Path);
+
+ // Proceed to sort the file
+ }
+
+ private Series GetMatchingSeries(string seriesName, IEnumerable<Series> 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<Series, int> GetMatchScore(string sortedName, int? year, Series series)
+ {
+ var score = 0;
+
+ if (year.HasValue)
+ {
+ if (series.ProductionYear.HasValue && year.Value == series.ProductionYear.Value)
+ {
+ score++;
+ }
+ }
+
+ // TODO: Improve this
+ if (string.Equals(sortedName, series.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ score++;
+ }
+
+ return new Tuple<Series, int>(series, score);
+ }
+
+ private void DeleteLeftOverFiles(string path, IEnumerable<string> 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);
+ }
+ }
+ }
+
+ 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) { }
+ }
+ }
+}