diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
index d23106266..8f3f9b0a6 100644
--- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
+++ b/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -313,6 +313,13 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
{
var trigger = (ITaskTrigger)sender;
+ var configurableTask = ScheduledTask as IConfigurableScheduledTask;
+
+ if (configurableTask != null && !configurableTask.IsEnabled)
+ {
+ return;
+ }
+
Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
trigger.Stop();
diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index d02984bad..a5b8de554 100644
--- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -169,5 +169,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
{
get { return true; }
}
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
}
}
diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index e5cb7aa10..e5c5638c0 100644
--- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -124,5 +124,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
{
get { return true; }
}
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
}
}
diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs
index 9a65046bf..78f60632f 100644
--- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs
+++ b/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs
@@ -96,5 +96,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
{
get { return true; }
}
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
}
}
diff --git a/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
index 2ee4fb4b5..b38e6357e 100644
--- a/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
+++ b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs
@@ -45,6 +45,15 @@ namespace MediaBrowser.Common.ScheduledTasks
public interface IConfigurableScheduledTask
{
+ ///
+ /// Gets a value indicating whether this instance is hidden.
+ ///
+ /// true if this instance is hidden; otherwise, false.
bool IsHidden { get; }
+ ///
+ /// Gets a value indicating whether this instance is enabled.
+ ///
+ /// true if this instance is enabled; otherwise, false.
+ bool IsEnabled { get; }
}
}
diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs
index 67d78fa89..54ebf8914 100644
--- a/MediaBrowser.Controller/Library/TVUtils.cs
+++ b/MediaBrowser.Controller/Library/TVUtils.cs
@@ -331,6 +331,30 @@ namespace MediaBrowser.Controller.Library
return null;
}
+ public static string GetSeriesNameFromEpisodeFile(string fullPath)
+ {
+ var fl = fullPath.ToLower();
+ foreach (var r in EpisodeExpressions)
+ {
+ var m = r.Match(fl);
+ if (m.Success)
+ {
+ var g = m.Groups["seriesname"];
+ if (g != null)
+ {
+ var val = g.Value;
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ return val;
+ }
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
///
/// Gets the air days.
///
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index ad65f79b6..b5afab3e5 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -224,7 +224,7 @@ namespace MediaBrowser.Model.Configuration
public bool EnableAutomaticRestart { get; set; }
-
+ public FileSortingOptions FileSortingOptions { get; set; }
public LiveTvOptions LiveTvOptions { get; set; }
///
@@ -288,10 +288,12 @@ namespace MediaBrowser.Model.Configuration
BookOptions = new MetadataOptions
{
- MaxBackdrops = 1
+ MaxBackdrops = 1
};
LiveTvOptions = new LiveTvOptions();
+
+ FileSortingOptions = new FileSortingOptions();
}
}
@@ -313,4 +315,35 @@ namespace MediaBrowser.Model.Configuration
{
public int? GuideDays { get; set; }
}
+
+ public class FileSortingOptions
+ {
+ public bool IsEnabled { get; set; }
+ public int MinFileSizeMb { get; set; }
+ public string[] LeftOverFileExtensionsToDelete { get; set; }
+ public string[] TvWatchLocations { get; set; }
+
+ public string SeasonFolderPattern { get; set; }
+
+ public string SeasonZeroFolderName { get; set; }
+
+ public bool OverwriteExistingEpisodes { get; set; }
+
+ public bool DeleteEmptyFolders { get; set; }
+
+ public FileSortingOptions()
+ {
+ MinFileSizeMb = 50;
+
+ LeftOverFileExtensionsToDelete = new[] {
+ ".nfo",
+ ".txt"
+ };
+
+ TvWatchLocations = new string[] { };
+
+ SeasonFolderPattern = "Season %s";
+ SeasonZeroFolderName = "Season 0";
+ }
+ }
}
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 progress)
+ {
+ return Task.Run(() => SortFiles(cancellationToken, progress), cancellationToken);
+ }
+
+ private void SortFiles(CancellationToken cancellationToken, IProgress 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 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()
+ .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 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 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 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;
+
+ 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, score);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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/LiveTv/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
index 1edd79d69..1f61d1a4f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
@@ -54,5 +54,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
{
get { return _liveTvManager.ActiveService == null; }
}
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
index 855183b97..9c1be678d 100644
--- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
+++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
@@ -117,9 +117,11 @@
+
+
diff --git a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs
index 900299667..455bdeb9c 100644
--- a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs
+++ b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs
@@ -135,6 +135,10 @@ namespace MediaBrowser.Server.Implementations.Udp
{
break;
}
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error in StartListening", ex);
+ }
}
}