/// <summary> /// Get the new series name /// </summary> /// <param name="series"></param> /// <param name="options"></param> /// <returns></returns> private string GetSeriesDirectoryName(Series series, TvFileOrganizationOptions options) { var seriesName = series.Name; var serieYear = series.ProductionYear; var seriesFullName = seriesName; if (series.ProductionYear.HasValue) { seriesFullName = string.Format("{0} ({1})", seriesFullName, series.ProductionYear); } var seasonFolderName = options.SeriesFolderPattern. Replace("%sn", seriesName) .Replace("%s.n", seriesName.Replace(" ", ".")) .Replace("%s_n", seriesName.Replace(" ", "_")) .Replace("%sy", serieYear.ToString()) .Replace("%fn", seriesFullName); return(_fileSystem.GetValidFilename(seasonFolderName)); }
/// <summary> /// Get the new series name. /// </summary> private string GetSeriesDirectoryName(Series series, TvFileOrganizationOptions options) { var seriesName = series.Name; var seriesYear = series.ProductionYear; var seriesFullName = seriesName; if (series.ProductionYear.HasValue) { seriesFullName = $"{seriesFullName} ({series.ProductionYear})"; } var seasonFolderName = options.SeriesFolderPattern. Replace("%sn", seriesName, StringComparison.Ordinal) .Replace("%s.n", seriesName.Replace(' ', '.'), StringComparison.Ordinal) .Replace("%s_n", seriesName.Replace(' ', '_'), StringComparison.Ordinal) .Replace("%sy", seriesYear.ToString(), StringComparison.Ordinal) .Replace("%fn", seriesFullName, StringComparison.Ordinal); return(_fileSystem.GetValidFilename(seasonFolderName)); }
private Task OrganizeEpisode( string sourcePath, string seriesName, int?seriesYear, int?seasonNumber, int?episodeNumber, int?endingEpiosdeNumber, DateTime?premiereDate, TvFileOrganizationOptions options, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { var series = GetMatchingSeries(seriesName, seriesYear, string.Empty, result); if (series == null) { series = AutoDetectSeries(seriesName, null, options, cancellationToken).Result; if (series == null) { var msg = "Unable to find series in library matching name " + seriesName; result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; _logger.LogWarning(msg); return(Task.FromResult(true)); } } return(OrganizeEpisode( sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options, rememberCorrection, result, cancellationToken)); }
/// <summary> /// Gets the season folder path. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) { var path = series.Path; if (ContainsEpisodesWithoutSeasonFolders(series)) { return(path); } if (seasonNumber == 0) { return(Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName))); } var seasonFolderName = options.SeasonFolderPattern .Replace("%s", seasonNumber.ToString(_usCulture), StringComparison.Ordinal) .Replace("%0s", seasonNumber.ToString("00", _usCulture), StringComparison.Ordinal) .Replace("%00s", seasonNumber.ToString("000", _usCulture), StringComparison.Ordinal); return(Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName))); }
private Season GetMatchingSeason(Series series, Episode episode, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var season = episode.Season; if (season == null) { if (!IsNewSeries(series)) { season = series .GetRecursiveChildren().OfType <Season>() .FirstOrDefault(e => e.IndexNumber == episode.ParentIndexNumber && e.LocationType == LocationType.FileSystem); } if (season == null) { if (!episode.ParentIndexNumber.HasValue) { var msg = string.Format("No season found for {0} season {1} episode {2}", series.Name, episode.ParentIndexNumber, episode.IndexNumber); _logger.Warn(msg); throw new OrganizationException(msg); } season = new Season { Id = Guid.NewGuid(), SeriesId = series.InternalId, IndexNumber = episode.ParentIndexNumber, }; } } if (string.IsNullOrEmpty(season.Path)) { season.Path = GetSeasonFolderPath(series, episode.ParentIndexNumber.Value, options); } return(season); }
private async Task OrganizeEpisode(string sourcePath, string seriesName, int?seriesYear, int?seasonNumber, int?episodeNumber, int?endingEpiosdeNumber, DateTime?premiereDate, TvFileOrganizationOptions options, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { var series = GetMatchingSeries(seriesName, seriesYear, null, result); if (series == null) { series = await AutoDetectSeries(seriesName, seriesYear, options, cancellationToken).ConfigureAwait(false); 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; } } await OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options, rememberCorrection, result, cancellationToken).ConfigureAwait(false); }
private Season GetMatchingSeason(Series series, Episode episode, TvFileOrganizationOptions options) { var season = episode.Season; if (season == null) { season = series .GetRecursiveChildren().OfType <Season>() .FirstOrDefault(e => e.IndexNumber == episode.ParentIndexNumber && e.LocationType == LocationType.FileSystem); if (season == null) { if (!episode.ParentIndexNumber.HasValue) { var msg = $"No season found for {series.Name} season {episode.ParentIndexNumber} episode {episode.IndexNumber}."; _logger.LogWarning(msg); throw new OrganizationException(msg); } season = new Season { Id = Guid.NewGuid(), SeriesId = series.Id, IndexNumber = episode.ParentIndexNumber, }; } } // If the season path is missing, compute it and create the directory on the filesystem if (string.IsNullOrEmpty(season.Path)) { season.Path = GetSeasonFolderPath(series, episode.ParentIndexNumber.Value, options); Directory.CreateDirectory(season.Path); } return(season); }
private async Task OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int?endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); // Proceed to sort the file var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, cancellationToken).ConfigureAwait(false); 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 fileExists = File.Exists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting) { if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; return; } if (fileExists || otherDuplicatePaths.Count > 0) { result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; result.DuplicatePaths = otherDuplicatePaths; return; } } PerformFileSorting(options, result); if (overwriteExisting) { foreach (var path in otherDuplicatePaths) { _logger.Debug("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); try { File.Delete(path); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } }
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int?endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); 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))); }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="premiereDate">The premiere date.</param> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> private async Task <string> GetNewPath(string sourcePath, Series series, int?seasonNumber, int?episodeNumber, int?endingEpisodeNumber, DateTime?premiereDate, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var episodeInfo = new EpisodeInfo { IndexNumber = episodeNumber, IndexNumberEnd = endingEpisodeNumber, MetadataCountryCode = series.GetPreferredMetadataCountryCode(), MetadataLanguage = series.GetPreferredMetadataLanguage(), ParentIndexNumber = seasonNumber, SeriesProviderIds = series.ProviderIds, PremiereDate = premiereDate }; var searchResults = await _providerManager.GetRemoteSearchResults <Episode, EpisodeInfo>(new RemoteSearchQuery <EpisodeInfo> { SearchInfo = episodeInfo }, cancellationToken).ConfigureAwait(false); var episode = searchResults.FirstOrDefault(); if (episode == null) { var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); _logger.Warn(msg); return(null); } var episodeName = episode.Name; //if (string.IsNullOrWhiteSpace(episodeName)) //{ // var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); // _logger.Warn(msg); // return null; //} seasonNumber = seasonNumber ?? episode.ParentIndexNumber; episodeNumber = episodeNumber ?? episode.IndexNumber; var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options); // MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256 // Usually newPath would include the drive component, but use 256 to be sure var maxFilenameLength = 256 - newPath.Length; if (!newPath.EndsWith(@"\")) { // Remove 1 for missing backslash combining path and filename maxFilenameLength--; } // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt) maxFilenameLength -= 4; var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength); if (string.IsNullOrEmpty(episodeFileName)) { // cause failure return(string.Empty); } newPath = Path.Combine(newPath, episodeFileName); return(newPath); }
public async Task <FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var result = _organizationService.GetResult(request.ResultId); var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); await OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, options, true, result, cancellationToken).ConfigureAwait(false); await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return(result); }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private async Task<string> GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var episodeInfo = new EpisodeInfo { IndexNumber = episodeNumber, IndexNumberEnd = endingEpisodeNumber, MetadataCountryCode = series.GetPreferredMetadataCountryCode(), MetadataLanguage = series.GetPreferredMetadataLanguage(), ParentIndexNumber = seasonNumber, SeriesProviderIds = series.ProviderIds }; var searchResults = await _providerManager.GetRemoteSearchResults<Episode, EpisodeInfo>(new RemoteSearchQuery<EpisodeInfo> { SearchInfo = episodeInfo }, cancellationToken).ConfigureAwait(false); var episode = searchResults.FirstOrDefault(); if (episode == null) { return null; } var newPath = GetSeasonFolderPath(series, seasonNumber, options); 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).Trim(); episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); 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)); }
private async Task <Series> AutoDetectSeries( string seriesName, int?seriesYear, TvFileOrganizationOptions options, CancellationToken cancellationToken) { if (options.AutoDetectSeries) { RemoteSearchResult finalResult = null; #region Search One var seriesInfo = new SeriesInfo { Name = seriesName, Year = seriesYear }; var searchResultsTask = await _providerManager.GetRemoteSearchResults <Series, SeriesInfo>(new RemoteSearchQuery <SeriesInfo> { SearchInfo = seriesInfo }, cancellationToken); #endregion // Group series by name and year (if 2 series with the exact same name, the same year ...) var groupedResult = searchResultsTask.GroupBy(p => new { p.Name, p.ProductionYear }, p => p, (key, g) => new { Key = key, Result = g.ToList() }).ToList(); if (groupedResult.Count == 1) { finalResult = groupedResult.First().Result.First(); } else if (groupedResult.Count > 1) { var filtredResult = groupedResult .Select(i => new { Ref = i, Score = NameUtils.GetMatchScore(seriesName, seriesYear, i.Key.Name, i.Key.ProductionYear) }) .Where(i => i.Score > 0) .OrderByDescending(i => i.Score) .Select(i => i.Ref) .FirstOrDefault(); finalResult = filtredResult?.Result.First(); } if (finalResult != null) { // We are in the good position, we can create the item var organizationRequest = new EpisodeFileOrganizationRequest { NewSeriesName = finalResult.Name, NewSeriesProviderIds = finalResult.ProviderIds, NewSeriesYear = finalResult.ProductionYear, TargetFolder = options.DefaultSeriesLibraryPath }; return(await CreateNewSeries(organizationRequest, finalResult, options, cancellationToken).ConfigureAwait(false)); } } return(null); }
private async Task OrganizeEpisode(string sourcePath, Series series, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); // Proceed to sort the file var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, cancellationToken).ConfigureAwait(false); 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 fileExists = File.Exists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting) { if (fileExists || otherDuplicatePaths.Count > 0) { result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; result.DuplicatePaths = otherDuplicatePaths; return; } if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { _logger.Info("File {0} already copied to new path {1}, stopping organization", sourcePath, newPath); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; return; } } PerformFileSorting(options, result); if (overwriteExisting) { foreach (var path in otherDuplicatePaths) { _logger.Debug("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); try { File.Delete(path); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } }
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); } } }
/// <summary> /// Perform organization for the TV watch folders. /// </summary> /// <param name="options">The organization options.</param> /// <param name="progress">The <see cref="IProgress{T}"/> to use for reporting operation progress.</param> /// <param name="cancellationToken">A cancellation token for the operation.</param> /// <returns>A task representing the operation completion.</returns> public async Task Organize( TvFileOrganizationOptions options, IProgress <double> progress, CancellationToken cancellationToken) { var libraryFolderPaths = _libraryManager.GetVirtualFolders().SelectMany(i => i.Locations).ToList(); var watchLocations = options.WatchLocations .Where(i => IsValidWatchLocation(i, libraryFolderPaths)) .ToList(); var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) .OrderBy(_fileSystem.GetCreationTimeUtc) .Where(i => EnableOrganization(i, options)) .ToList(); var processedFolders = new HashSet <string>(); progress.Report(10); if (eligibleFiles.Count > 0) { var numComplete = 0; var organizer = new EpisodeFileOrganizer( _organizationService, _fileSystem, _loggerFactory.CreateLogger <EpisodeFileOrganizer>(), _libraryManager, _libraryMonitor, _providerManager); foreach (var file in eligibleFiles) { cancellationToken.ThrowIfCancellationRequested(); try { var result = await organizer.OrganizeEpisodeFile(file.FullName, options, cancellationToken).ConfigureAwait(false); if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) { processedFolders.Add(file.DirectoryName); } } catch (OperationCanceledException) { break; } catch (Exception ex) { _logger.LogError(ex, "Error organizing episode {0}", file.FullName); } numComplete++; double percent = numComplete; percent /= eligibleFiles.Count; progress.Report(10 + (89 * percent)); } } cancellationToken.ThrowIfCancellationRequested(); progress.Report(99); var deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) .ToList(); // Normal Clean Clean(processedFolders, watchLocations, options.DeleteEmptyFolders, deleteExtensions); // Extended Clean if (options.ExtendedClean) { Clean(watchLocations, watchLocations, options.DeleteEmptyFolders, deleteExtensions); } progress.Report(100); }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> 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<Episode>() .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; }
public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress <double> progress) { var watchLocations = options.WatchLocations.ToList(); var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) .OrderBy(_fileSystem.GetCreationTimeUtc) .Where(i => EnableOrganization(i, options)) .ToList(); var processedFolders = new HashSet <string>(); progress.Report(10); if (eligibleFiles.Count > 0) { var numComplete = 0; foreach (var file in eligibleFiles) { var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); try { var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) { processedFolders.Add(file.DirectoryName); } } catch (Exception ex) { _logger.ErrorException("Error organizing episode {0}", ex, file); } numComplete++; double percent = numComplete; percent /= eligibleFiles.Count; progress.Report(10 + (89 * percent)); } } cancellationToken.ThrowIfCancellationRequested(); progress.Report(99); foreach (var path in processedFolders) { var deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) .ToList(); if (deleteExtensions.Count > 0) { DeleteLeftOverFiles(path, deleteExtensions); } if (options.DeleteEmptyFolders) { if (!IsWatchFolder(path, watchLocations)) { DeleteEmptyFolders(path); } } } progress.Report(100); }
public AutoOrganizeOptions() { TvOptions = new TvFileOrganizationOptions(); }
private void SetEpisodeFileName(string sourcePath, Series series, Season season, Episode episode, TvFileOrganizationOptions options) { var seriesName = _fileSystem.GetValidFilename(series.Name).Trim(); var episodeTitle = _fileSystem.GetValidFilename(episode.Name).Trim(); if (!episode.IndexNumber.HasValue || !season.IndexNumber.HasValue) { throw new OrganizationException("GetEpisodeFileName: Mandatory param as missing!"); } var endingEpisodeNumber = episode.IndexNumberEnd; var episodeNumber = episode.IndexNumber.Value; var seasonNumber = season.IndexNumber.Value; var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; if (string.IsNullOrWhiteSpace(pattern)) { throw new OrganizationException("GetEpisodeFileName: Configured episode name pattern is empty!"); } var result = pattern.Replace("%sn", seriesName, StringComparison.Ordinal) .Replace("%s.n", seriesName.Replace(' ', '.'), StringComparison.Ordinal) .Replace("%s_n", seriesName.Replace(' ', '_'), StringComparison.Ordinal) .Replace("%s", seasonNumber.ToString(_usCulture), StringComparison.Ordinal) .Replace("%0s", seasonNumber.ToString("00", _usCulture), StringComparison.Ordinal) .Replace("%00s", seasonNumber.ToString("000", _usCulture), StringComparison.Ordinal) .Replace("%ext", sourceExtension, StringComparison.Ordinal) .Replace("%en", "%#1", StringComparison.Ordinal) .Replace("%e.n", "%#2", StringComparison.Ordinal) .Replace("%e_n", "%#3", StringComparison.Ordinal) .Replace("%fn", Path.GetFileNameWithoutExtension(sourcePath), StringComparison.Ordinal); if (endingEpisodeNumber.HasValue) { result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture), StringComparison.Ordinal) .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture), StringComparison.Ordinal) .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture), StringComparison.Ordinal); } result = result.Replace("%e", episodeNumber.ToString(_usCulture), StringComparison.Ordinal) .Replace("%0e", episodeNumber.ToString("00", _usCulture), StringComparison.Ordinal) .Replace("%00e", episodeNumber.ToString("000", _usCulture), StringComparison.Ordinal); if (result.Contains("%#", StringComparison.Ordinal)) { result = result.Replace("%#1", episodeTitle, StringComparison.Ordinal) .Replace("%#2", episodeTitle.Replace(' ', '.'), StringComparison.Ordinal) .Replace("%#3", episodeTitle.Replace(' ', '_'), StringComparison.Ordinal); } // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters episode.Path = Path.Combine(season.Path, _fileSystem.GetValidFilename(result).Trim()); }
private Task OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) { 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 Task.FromResult(true); } return OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, result, cancellationToken); }
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 fileExists = File.Exists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); if (!overwriteExisting && (fileExists || otherDuplicatePaths.Count > 0)) { result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = string.Empty; result.DuplicatePaths = otherDuplicatePaths; return; } PerformFileSorting(options, result); if (overwriteExisting) { foreach (var path in otherDuplicatePaths) { _logger.Debug("Removing duplicate episode {0}", path); try { File.Delete(path); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode", ex, path); } } } }
private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) { _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); Directory.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); var copy = File.Exists(result.TargetPath); try { if (copy || options.CopyOriginalFile) { 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 { _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true); } if (copy && !options.CopyOriginalFile) { try { File.Delete(result.OriginalPath); } catch (Exception ex) { _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); } } }
private Task OrganizeEpisode( string sourcePath, Series series, Episode episode, TvFileOrganizationOptions options, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.LogInformation("Sorting file {0} into series {1}", sourcePath, series.Path); var originalExtractedSeriesString = result.ExtractedName; bool isNew = string.IsNullOrWhiteSpace(result.Id); if (isNew) { _organizationService.SaveResult(result, cancellationToken); } if (!_organizationService.AddToInProgressList(result, isNew)) { throw new OrganizationException("File is currently processed otherwise. Please try again later."); } try { // Proceed to sort the file var newPath = episode.Path; if (string.IsNullOrEmpty(newPath)) { var msg = $"Unable to sort {sourcePath} because target path could not be determined."; throw new OrganizationException(msg); } _logger.LogInformation("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; var fileExists = File.Exists(result.TargetPath); var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, episode); if (!options.OverwriteExistingEpisodes) { if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) { var msg = $"File '{sourcePath}' already copied to new path '{newPath}', stopping organization"; _logger.LogInformation(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; return(Task.CompletedTask); } if (fileExists) { var msg = $"File '{sourcePath}' already exists as '{newPath}', stopping organization"; _logger.LogInformation(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; result.TargetPath = newPath; return(Task.CompletedTask); } if (otherDuplicatePaths.Count > 0) { var msg = $"File '{sourcePath}' already exists as these:'{string.Join("', '", otherDuplicatePaths)}'. Stopping organization"; _logger.LogInformation(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; result.DuplicatePaths = otherDuplicatePaths; return(Task.CompletedTask); } } PerformFileSorting(options, result); if (options.OverwriteExistingEpisodes) { var hasRenamedFiles = false; foreach (var path in otherDuplicatePaths) { _logger.LogDebug("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); var renameRelatedFiles = !hasRenamedFiles && string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); if (renameRelatedFiles) { hasRenamedFiles = true; } try { DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); } catch (IOException ex) { _logger.LogError(ex, "Error removing duplicate episode: {0}", path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } } catch (Exception ex) { result.Status = FileSortingStatus.Failure; result.StatusMessage = ex.Message; _logger.LogError(ex, "Caught a generic exception while organizing an episode"); return(Task.CompletedTask); } finally { _organizationService.RemoveFromInprogressList(result); } if (rememberCorrection) { SaveSmartMatchString(originalExtractedSeriesString, series, cancellationToken); } return(Task.CompletedTask); }
/// <summary> /// Gets the season folder path. /// </summary> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) { // If there's already a season folder, use that var season = series .RecursiveChildren .OfType<Season>() .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)); }
/// <summary> /// Organize an episode file. /// </summary> /// <param name="path">The path to the episode file.</param> /// <param name="options">The options to use for organizing the file.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param> /// <returns>A task representing the file organization operation and containing the operation result.</returns> public async Task <FileOrganizationResult> OrganizeEpisodeFile( string path, TvFileOrganizationOptions options, CancellationToken cancellationToken) { _logger.LogInformation("Sorting file {0}", path); var result = new FileOrganizationResult { Date = DateTime.UtcNow, OriginalPath = path, OriginalFileName = Path.GetFileName(path), Type = FileOrganizerType.Unknown, FileSize = _fileSystem.GetFileInfo(path).Length }; try { if (_libraryMonitor.IsPathLocked(path)) { result.Status = FileSortingStatus.Failure; result.StatusMessage = "Path is locked by other processes. Please try again later."; _logger.LogInformation("Auto-organize Path is locked by other processes. Please try again later."); return(result); } var namingOptions = GetNamingOptionsInternal(); var resolver = new EpisodeResolver(namingOptions); var episodeInfo = resolver.Resolve(path, false) ?? new Naming.TV.EpisodeInfo(string.Empty); var seriesName = episodeInfo.SeriesName; int?seriesYear = null; if (!string.IsNullOrEmpty(seriesName)) { var seriesParseResult = _libraryManager.ParseName(seriesName); seriesName = seriesParseResult.Name; seriesYear = seriesParseResult.Year; } if (string.IsNullOrWhiteSpace(seriesName)) { seriesName = episodeInfo.SeriesName; } if (!string.IsNullOrEmpty(seriesName)) { var seasonNumber = episodeInfo.SeasonNumber; result.ExtractedSeasonNumber = seasonNumber; // Passing in true will include a few extra regex's var episodeNumber = episodeInfo.EpisodeNumber; result.ExtractedEpisodeNumber = episodeNumber; var premiereDate = episodeInfo.IsByDate ? new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) : (DateTime?)null; if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue)) { if (episodeInfo.IsByDate) { _logger.LogDebug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value); } else { _logger.LogDebug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber); } // We detected an airdate or (an season number and an episode number) // We have all the chance that the media type is an Episode // if an earlier result exist with an different type, we update it result.Type = CurrentFileOrganizerType; var endingEpisodeNumber = episodeInfo.EndingEpisodeNumber; result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; await OrganizeEpisode( path, seriesName, seriesYear, seasonNumber, episodeNumber, endingEpisodeNumber, premiereDate, options, false, result, cancellationToken).ConfigureAwait(false); } else { var msg = "Unable to determine episode number from " + path; result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; _logger.LogWarning(msg); } } else { var msg = "Unable to determine series name from " + path; result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; _logger.LogWarning(msg); } // Handle previous result var previousResult = _organizationService.GetResultBySourcePath(path); if ((previousResult != null && result.Type == FileOrganizerType.Unknown) || (previousResult?.Status == result.Status && previousResult?.StatusMessage == result.StatusMessage && result.Status != FileSortingStatus.Success)) { // Don't keep saving the same result over and over if nothing has changed return(previousResult); } } catch (OrganizationException ex) { result.Status = FileSortingStatus.Failure; result.StatusMessage = ex.Message; } catch (Exception ex) { result.Status = FileSortingStatus.Failure; result.StatusMessage = ex.Message; _logger.LogError(ex, "Error organizing file"); } _organizationService.SaveResult(result, CancellationToken.None); return(result); }
public async Task<FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting, CancellationToken cancellationToken) { _logger.Info("Sorting file {0}", path); var result = new FileOrganizationResult { Date = DateTime.UtcNow, OriginalPath = path, OriginalFileName = Path.GetFileName(path), Type = FileOrganizerType.Episode, FileSize = new FileInfo(path).Length }; 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; await OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, result, cancellationToken).ConfigureAwait(false); } 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); } var previousResult = _organizationService.GetResultBySourcePath(path); if (previousResult != null) { // Don't keep saving the same result over and over if nothing has changed if (previousResult.Status == result.Status && result.Status != FileSortingStatus.Success) { return previousResult; } } await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return result; }
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int?endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); if (string.IsNullOrWhiteSpace(episodeTitle)) { episodeTitle = string.Empty; } else { episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); } var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; if (string.IsNullOrWhiteSpace(pattern)) { throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!"); } 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", "%#1") .Replace("%e.n", "%#2") .Replace("%e_n", "%#3"); 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)); } result = result.Replace("%e", episodeNumber.ToString(_usCulture)) .Replace("%0e", episodeNumber.ToString("00", _usCulture)) .Replace("%00e", episodeNumber.ToString("000", _usCulture)); if (result.Contains("%#")) { result = result.Replace("%#1", episodeTitle) .Replace("%#2", episodeTitle.Replace(" ", ".")) .Replace("%#3", episodeTitle.Replace(" ", "_")); } // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters return(_fileSystem.GetValidFilename(result).Trim()); }
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int?endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int?maxLength) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); if (string.IsNullOrWhiteSpace(episodeTitle)) { episodeTitle = string.Empty; } else { episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); } 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", "%#1") .Replace("%e.n", "%#2") .Replace("%e_n", "%#3"); 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)); } result = result.Replace("%e", episodeNumber.ToString(_usCulture)) .Replace("%0e", episodeNumber.ToString("00", _usCulture)) .Replace("%00e", episodeNumber.ToString("000", _usCulture)); if (maxLength.HasValue && result.Contains("%#")) { // Substract 3 for the temp token length (%#1, %#2 or %#3) int maxRemainingTitleLength = maxLength.Value - result.Length + 3; string shortenedEpisodeTitle = string.Empty; if (maxRemainingTitleLength > 5) { // A title with fewer than 5 letters wouldn't be of much value shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length)); } result = result.Replace("%#1", shortenedEpisodeTitle) .Replace("%#2", shortenedEpisodeTitle.Replace(" ", ".")) .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_")); } if (maxLength.HasValue && result.Length > maxLength.Value) { // There may be cases where reducing the title length may still not be sufficient to // stay below maxLength var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength); _logger.Warn(msg); return(string.Empty); } return(result); }
private void OrganizeEpisode(string sourcePath, Series series, Episode episode, TvFileOrganizationOptions options, bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); var originalExtractedSeriesString = result.ExtractedName; bool isNew = string.IsNullOrWhiteSpace(result.Id); if (isNew) { _organizationService.SaveResult(result, cancellationToken); } if (!_organizationService.AddToInProgressList(result, isNew)) { var msg = string.Format("File {0} is currently processed otherwise. Please try again later.", sourcePath); _logger.Warn(msg + " Stopping organization"); result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; return; } try { // Proceed to sort the file var newPath = episode.Path; if (string.IsNullOrEmpty(newPath)) { var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); _logger.Info(msg + " Stopping organization"); result.Status = FileSortingStatus.Failure; result.StatusMessage = msg; return; } _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); result.TargetPath = newPath; var fileExists = _fileSystem.FileExists(result.TargetPath); if (options.SingleEpisodeVersion) //add value here to ensure returned to user regardless of result below { result.DuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, episode); _logger.Info(string.Format("otherDuplicatePaths: '{0}'", string.Join("', '", result.DuplicatePaths))); } if (!options.OverwriteExistingEpisodes) { if (options.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath) && result.DuplicatePaths.Count == 1) { var msg = string.Format("File '{0}' already copied to new path '{1}.'", sourcePath, newPath); _logger.Info(msg + " Stopping organization"); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; return; } if (result.DuplicatePaths.Count > 0) { var msg = string.Format("File '{0}' already exists as: '{1}'.", sourcePath, string.Join("', '", result.DuplicatePaths), (result.DuplicatePaths.Count > 1 ? "these" : "")); _logger.Info(msg + " Stopping organization"); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; return; } if (fileExists) { var msg = string.Format("File '{0}' already exists as '{1}'.", sourcePath, newPath); _logger.Info(msg + " Stopping organization"); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; result.TargetPath = newPath; return; } } PerformFileSorting(options, result); if (options.SingleEpisodeVersion) { var hasRenamedFiles = false; foreach (var path in result.DuplicatePaths) { if (!string.Equals(path, newPath, StringComparison.OrdinalIgnoreCase))//dont remove file matching destination path { _logger.Info("Removing duplicate episode {0}", path); _libraryMonitor.ReportFileSystemChangeBeginning(path); var renameRelatedFiles = !hasRenamedFiles && string.Equals(_fileSystem.GetDirectoryName(path), _fileSystem.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); if (renameRelatedFiles) { hasRenamedFiles = true; } try { DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); } catch (IOException ex) { _logger.ErrorException("Error removing duplicate episode {0}", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } } } catch (Exception ex) { result.Status = FileSortingStatus.Failure; result.StatusMessage = string.Format("Error sorting episode: '{0}'.", ex.Message); _logger.ErrorException("Error sorting episode: {0}", ex, episode.Path); return; } finally { _organizationService.RemoveFromInprogressList(result); } if (rememberCorrection) { SaveSmartMatchString(originalExtractedSeriesString, series.Name, cancellationToken); } }
private Task OrganizeEpisode(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int?endingEpiosdeNumber, TvFileOrganizationOptions options, bool overwriteExisting, FileOrganizationResult result, CancellationToken cancellationToken) { 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(Task.FromResult(true)); } return(OrganizeEpisode(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, options, overwriteExisting, result, cancellationToken)); }
public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress <double> progress) { var minFileBytes = options.MinFileSizeMb * 1024 * 1024; var watchLocations = options.WatchLocations.ToList(); var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) .OrderBy(_fileSystem.GetCreationTimeUtc) .Where(i => _libraryManager.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, _libraryMonitor, _providerManager); var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).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) { var deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) .ToList(); if (deleteExtensions.Count > 0) { DeleteLeftOverFiles(path, deleteExtensions); } if (options.DeleteEmptyFolders) { foreach (var subfolder in GetDirectories(path).ToList()) { DeleteEmptyFolders(subfolder); } } } if (scanLibrary) { await _libraryManager.ValidateMediaLibrary(new Progress <double>(), CancellationToken.None) .ConfigureAwait(false); } progress.Report(100); }
/// <summary> /// Gets the new path. /// </summary> /// <param name="sourcePath">The source path.</param> /// <param name="series">The series.</param> /// <param name="seasonNumber">The season number.</param> /// <param name="episodeNumber">The episode number.</param> /// <param name="endingEpisodeNumber">The ending episode number.</param> /// <param name="options">The options.</param> /// <returns>System.String.</returns> private async Task <string> GetNewPath(string sourcePath, Series series, int seasonNumber, int episodeNumber, int?endingEpisodeNumber, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var episodeInfo = new EpisodeInfo { IndexNumber = episodeNumber, IndexNumberEnd = endingEpisodeNumber, MetadataCountryCode = series.GetPreferredMetadataCountryCode(), MetadataLanguage = series.GetPreferredMetadataLanguage(), ParentIndexNumber = seasonNumber, SeriesProviderIds = series.ProviderIds }; var searchResults = await _providerManager.GetRemoteSearchResults <Episode, EpisodeInfo>(new RemoteSearchQuery <EpisodeInfo> { SearchInfo = episodeInfo }, cancellationToken).ConfigureAwait(false); var episode = searchResults.FirstOrDefault(); if (episode == null) { _logger.Warn("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); return(null); } var newPath = GetSeasonFolderPath(series, seasonNumber, options); var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber, episodeNumber, endingEpisodeNumber, episode.Name, options); newPath = Path.Combine(newPath, episodeFileName); return(newPath); }
public async Task<FileOrganizationResult> OrganizeWithCorrection(EpisodeFileOrganizationRequest request, TvFileOrganizationOptions options, CancellationToken cancellationToken) { var result = _organizationService.GetResult(request.ResultId); var series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); await OrganizeEpisode(result.OriginalPath, series, request.SeasonNumber, request.EpisodeNumber, request.EndingEpisodeNumber, _config.Configuration.TvFileOrganizationOptions, true, result, cancellationToken).ConfigureAwait(false); await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return result; }
public async Task <FileOrganizationResult> OrganizeEpisodeFile(string path, TvFileOrganizationOptions options, bool overwriteExisting, CancellationToken cancellationToken) { _logger.Info("Sorting file {0}", path); var result = new FileOrganizationResult { Date = DateTime.UtcNow, OriginalPath = path, OriginalFileName = Path.GetFileName(path), Type = FileOrganizerType.Episode, FileSize = new FileInfo(path).Length }; var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var resolver = new Naming.TV.EpisodeResolver(namingOptions, new Naming.Logging.NullLogger()); var episodeInfo = resolver.Resolve(path, FileInfoType.File) ?? new Naming.TV.EpisodeInfo(); var seriesName = episodeInfo.SeriesName; if (!string.IsNullOrEmpty(seriesName)) { var season = episodeInfo.SeasonNumber; result.ExtractedSeasonNumber = season; if (season.HasValue) { // Passing in true will include a few extra regex's var episode = episodeInfo.EpisodeNumber; 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 = episodeInfo.EndingEpsiodeNumber; result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; await OrganizeEpisode(path, seriesName, season.Value, episode.Value, endingEpisodeNumber, options, overwriteExisting, result, cancellationToken).ConfigureAwait(false); } 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); } var previousResult = _organizationService.GetResultBySourcePath(path); if (previousResult != null) { // Don't keep saving the same result over and over if nothing has changed if (previousResult.Status == result.Status && result.Status != FileSortingStatus.Success) { return(previousResult); } } await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return(result); }
public AutoOrganizeOptions() { TvOptions = new TvFileOrganizationOptions(); SmartMatchInfos = new SmartMatchInfo[] {}; }
public async Task Organize(TvFileOrganizationOptions options, CancellationToken cancellationToken, IProgress<double> 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, _libraryMonitor, _providerManager); var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).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) { var deleteExtensions = options.LeftOverFileExtensionsToDelete .Select(i => i.Trim().TrimStart('.')) .Where(i => !string.IsNullOrEmpty(i)) .Select(i => "." + i) .ToList(); if (deleteExtensions.Count > 0) { DeleteLeftOverFiles(path, deleteExtensions); } if (options.DeleteEmptyFolders) { foreach (var subfolder in GetDirectories(path).ToList()) { DeleteEmptyFolders(subfolder); } } } if (scanLibrary) { await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None) .ConfigureAwait(false); } progress.Report(100); }
private void SetEpisodeFileName(string sourcePath, string seriesName, Season season, Episode episode, TvFileOrganizationOptions options) { seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); var episodeTitle = _fileSystem.GetValidFilename(episode.Name).Trim(); if (options.AllowTBA == false && episodeTitle == "TBA") { throw new OrganizationException("Returned metadata title is 'TBA'. Current settings prevent this file from being moved."); } if (!episode.IndexNumber.HasValue || !season.IndexNumber.HasValue) { throw new OrganizationException("GetEpisodeFileName: Mandatory param as missing!"); } var endingEpisodeNumber = episode.IndexNumberEnd; var episodeNumber = episode.IndexNumber.Value; var seasonNumber = season.IndexNumber.Value; var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; if (string.IsNullOrWhiteSpace(pattern)) { throw new OrganizationException("GetEpisodeFileName: Configured episode name pattern is empty!"); } 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", "%#1") .Replace("%e.n", "%#2") .Replace("%e_n", "%#3") .Replace("%fn", Path.GetFileNameWithoutExtension(sourcePath)); 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)); } result = result.Replace("%e", episodeNumber.ToString(_usCulture)) .Replace("%0e", episodeNumber.ToString("00", _usCulture)) .Replace("%00e", episodeNumber.ToString("000", _usCulture)); if (result.Contains("%#")) { result = result.Replace("%#1", episodeTitle) .Replace("%#2", episodeTitle.Replace(" ", ".")) .Replace("%#3", episodeTitle.Replace(" ", "_")); } // Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters episode.Path = Path.Combine(season.Path, _fileSystem.GetValidFilename(result).Trim()); }