Ejemplo n.º 1
0
        /// <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));
        }
Ejemplo n.º 3
0
        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));
        }
Ejemplo n.º 4
0
        /// <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);
        }
Ejemplo n.º 7
0
        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);
        }
Ejemplo n.º 8
0
        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);
                    }
                }
            }
        }
Ejemplo n.º 9
0
        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)));
        }
Ejemplo n.º 10
0
        /// <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);
        }
Ejemplo n.º 11
0
        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);
        }
Ejemplo n.º 12
0
        /// <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;
        }
Ejemplo n.º 13
0
        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));
        }
Ejemplo n.º 14
0
        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);
        }
Ejemplo n.º 15
0
        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);
                    }
                }
            }
        }
Ejemplo n.º 16
0
        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);
        }
Ejemplo n.º 18
0
        /// <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;
        }
Ejemplo n.º 19
0
        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);
        }
Ejemplo n.º 20
0
 public AutoOrganizeOptions()
 {
     TvOptions = new TvFileOrganizationOptions();
 }
Ejemplo n.º 21
0
 public AutoOrganizeOptions()
 {
     TvOptions = new TvFileOrganizationOptions();
 }
Ejemplo n.º 22
0
        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());
        }
Ejemplo n.º 23
0
        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);
        }
Ejemplo n.º 24
0
        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);
                    }
                }
            }
        }
Ejemplo n.º 25
0
        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);
                }
            }
        }
Ejemplo n.º 26
0
        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);
        }
Ejemplo n.º 27
0
        /// <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));
        }
Ejemplo n.º 28
0
        /// <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);
        }
Ejemplo n.º 29
0
        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;
        }
Ejemplo n.º 30
0
        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());
        }
Ejemplo n.º 31
0
        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);
        }
Ejemplo n.º 32
0
        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);
            }
        }
Ejemplo n.º 33
0
        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));
        }
Ejemplo n.º 34
0
        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);
        }
Ejemplo n.º 35
0
        /// <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);
        }
Ejemplo n.º 36
0
        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;
        }
Ejemplo n.º 37
0
        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[] {};
 }
Ejemplo n.º 39
0
        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);
        }
Ejemplo n.º 40
0
        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());
        }