private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options) { var minFileBytes = options.MinFileSizeMb * 1024 * 1024; try { return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes; } catch (Exception ex) { _logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name); } return false; }
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; }
/// <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 .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber) .FirstOrDefault(); 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> /// 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; }
private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) { _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath); try { if (targetAlreadyExists || options.CopyOriginalFile) { _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true); } else { _fileSystem.MoveFile(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 (targetAlreadyExists && !options.CopyOriginalFile) { try { _fileSystem.DeleteFile(result.OriginalPath); } catch (Exception ex) { _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); } } }
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 PatternsLogger()); var episodeInfo = resolver.Resolve(path, false) ?? new Naming.TV.EpisodeInfo(); var 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.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value); } else { _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber); } var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber; result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; await OrganizeEpisode(path, seriesName, seasonNumber, episodeNumber, endingEpisodeNumber, premiereDate, 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 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 async Task OrganizeEpisode(string sourcePath, Series series, int? seasonNumber, int? episodeNumber, int? endingEpiosdeNumber, DateTime? premiereDate, 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, premiereDate, 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 = _fileSystem.FileExists(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) { var hasRenamedFiles = false; foreach (var path in otherDuplicatePaths) { _logger.Debug("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.ErrorException("Error removing duplicate episode", ex, path); } finally { _libraryMonitor.ReportFileSystemChangeComplete(path, true); } } } }
private Task OrganizeEpisode(string sourcePath, string seriesName, int? seasonNumber, int? episodeNumber, int? endingEpiosdeNumber, DateTime? premiereDate, 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, premiereDate, options, overwriteExisting, result, cancellationToken); }
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, null, options, true, result, cancellationToken).ConfigureAwait(false); await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); return result; }
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); try { var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); if (result.Status == FileSortingStatus.Success) { scanLibrary = true; } } 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 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 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="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; }