Browse Source

#680 - Support new episode file sorting

Luke Pulverenti 11 years ago
parent
commit
a9f2a72d0b

+ 7 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -313,6 +313,13 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
         {
         {
             var trigger = (ITaskTrigger)sender;
             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);
             Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
 
 
             trigger.Stop();
             trigger.Stop();

+ 5 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs

@@ -169,5 +169,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         {
         {
             get { return true; }
             get { return true; }
         }
         }
+
+        public bool IsEnabled
+        {
+            get { return true; }
+        }
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs

@@ -124,5 +124,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         {
         {
             get { return true; }
             get { return true; }
         }
         }
+
+        public bool IsEnabled
+        {
+            get { return true; }
+        }
     }
     }
 }
 }

+ 5 - 0
MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerTask.cs

@@ -96,5 +96,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks
         {
         {
             get { return true; }
             get { return true; }
         }
         }
+
+        public bool IsEnabled
+        {
+            get { return true; }
+        }
     }
     }
 }
 }

+ 9 - 0
MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs

@@ -45,6 +45,15 @@ namespace MediaBrowser.Common.ScheduledTasks
 
 
     public interface IConfigurableScheduledTask
     public interface IConfigurableScheduledTask
     {
     {
+        /// <summary>
+        /// Gets a value indicating whether this instance is hidden.
+        /// </summary>
+        /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
         bool IsHidden { get; }
         bool IsHidden { get; }
+        /// <summary>
+        /// Gets a value indicating whether this instance is enabled.
+        /// </summary>
+        /// <value><c>true</c> if this instance is enabled; otherwise, <c>false</c>.</value>
+        bool IsEnabled { get; }
     }
     }
 }
 }

+ 24 - 0
MediaBrowser.Controller/Library/TVUtils.cs

@@ -331,6 +331,30 @@ namespace MediaBrowser.Controller.Library
             return null;
             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;
+        }
+
         /// <summary>
         /// <summary>
         /// Gets the air days.
         /// Gets the air days.
         /// </summary>
         /// </summary>

+ 35 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -224,7 +224,7 @@ namespace MediaBrowser.Model.Configuration
 
 
         public bool EnableAutomaticRestart { get; set; }
         public bool EnableAutomaticRestart { get; set; }
 
 
-
+        public FileSortingOptions FileSortingOptions { get; set; }
         public LiveTvOptions LiveTvOptions { get; set; }
         public LiveTvOptions LiveTvOptions { get; set; }
 
 
         /// <summary>
         /// <summary>
@@ -288,10 +288,12 @@ namespace MediaBrowser.Model.Configuration
 
 
             BookOptions = new MetadataOptions
             BookOptions = new MetadataOptions
             {
             {
-                 MaxBackdrops = 1
+                MaxBackdrops = 1
             };
             };
 
 
             LiveTvOptions = new LiveTvOptions();
             LiveTvOptions = new LiveTvOptions();
+
+            FileSortingOptions = new FileSortingOptions();
         }
         }
     }
     }
 
 
@@ -313,4 +315,35 @@ namespace MediaBrowser.Model.Configuration
     {
     {
         public int? GuideDays { get; set; }
         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";
+        }
+    }
 }
 }

+ 96 - 0
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; }
+        }
+    }
+}

+ 186 - 0
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) { }
+        }
+    }
+}

+ 5 - 0
MediaBrowser.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs

@@ -54,5 +54,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
         {
         {
             get { return _liveTvManager.ActiveService == null; }
             get { return _liveTvManager.ActiveService == null; }
         }
         }
+
+        public bool IsEnabled
+        {
+            get { return true; }
+        }
     }
     }
 }
 }

+ 2 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -117,9 +117,11 @@
     <Compile Include="EntryPoints\Notifications\RemoteNotifications.cs" />
     <Compile Include="EntryPoints\Notifications\RemoteNotifications.cs" />
     <Compile Include="EntryPoints\Notifications\WebSocketNotifier.cs" />
     <Compile Include="EntryPoints\Notifications\WebSocketNotifier.cs" />
     <Compile Include="EntryPoints\RefreshUsersMetadata.cs" />
     <Compile Include="EntryPoints\RefreshUsersMetadata.cs" />
+    <Compile Include="FileSorting\TvFileSorter.cs" />
     <Compile Include="EntryPoints\UdpServerEntryPoint.cs" />
     <Compile Include="EntryPoints\UdpServerEntryPoint.cs" />
     <Compile Include="EntryPoints\ServerEventNotifier.cs" />
     <Compile Include="EntryPoints\ServerEventNotifier.cs" />
     <Compile Include="EntryPoints\UserDataChangeNotifier.cs" />
     <Compile Include="EntryPoints\UserDataChangeNotifier.cs" />
+    <Compile Include="FileSorting\SortingScheduledTask.cs" />
     <Compile Include="HttpServer\ContainerAdapter.cs" />
     <Compile Include="HttpServer\ContainerAdapter.cs" />
     <Compile Include="HttpServer\HttpListenerHost.cs" />
     <Compile Include="HttpServer\HttpListenerHost.cs" />
     <Compile Include="HttpServer\HttpResultFactory.cs" />
     <Compile Include="HttpServer\HttpResultFactory.cs" />

+ 4 - 0
MediaBrowser.Server.Implementations/Udp/UdpServer.cs

@@ -135,6 +135,10 @@ namespace MediaBrowser.Server.Implementations.Udp
                 {
                 {
                     break;
                     break;
                 }
                 }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error in StartListening", ex);
+                }
             }
             }
         }
         }