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