Fixed: Include extension when calculating maximum episode title length when renaming files

Fixed: Option to override max filename length with MAX_NAME environment variable

Closes #3888
pull/3919/head
Taloth Saldono 4 years ago
parent e6175581bd
commit 6efee036a8

@ -1,15 +1,68 @@
using System;
using System.IO;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Disk
{
public static class LongPathSupport
{
private static int MAX_PATH;
private static int MAX_NAME;
public static void Enable()
{
// Mono has an issue with enabling long path support via app.config.
// This works for both mono and .net on Windows.
AppContext.SetSwitch("Switch.System.IO.UseLegacyPathHandling", false);
AppContext.SetSwitch("Switch.System.IO.BlockLongPaths", false);
DetectLongPathLimits();
}
private static void DetectLongPathLimits()
{
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_PATH"), out MAX_PATH))
{
if (OsInfo.IsLinux)
{
MAX_PATH = 4096;
}
else
{
try
{
Path.GetDirectoryName($@"C:\{new string('a', 300)}\ab");
MAX_PATH = 4096;
}
catch
{
MAX_PATH = 260;
}
}
}
if (!int.TryParse(Environment.GetEnvironmentVariable("MAX_NAME"), out MAX_NAME))
{
MAX_NAME = 255;
}
}
public static int MaxFilePathLength
{
get
{
if (MAX_PATH == 0) DetectLongPathLimits();
return MAX_PATH;
}
}
public static int MaxFileNameLength
{
get
{
if (MAX_NAME == 0) DetectLongPathLimits();
return MAX_NAME;
}
}
}
}

@ -42,11 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests
.Build();
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.BuildFileName(It.IsAny<List<Episode>>(), It.IsAny<Series>(), It.IsAny<EpisodeFile>(), null, null))
.Returns("File Name");
Mocker.GetMock<IBuildFileNames>()
.Setup(s => s.BuildFilePath(It.IsAny<Series>(), It.IsAny<int>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(s => s.BuildFilePath(It.IsAny<List<Episode>>(), It.IsAny<Series>(), It.IsAny<EpisodeFile>(), It.IsAny<string>(), It.IsAny<NamingConfig>(), It.IsAny<List<string>>()))
.Returns(@"C:\Test\TV\Series\Season 01\File Name.avi".AsOsAgnostic());
Mocker.GetMock<IBuildFileNames>()

@ -1,9 +1,11 @@
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.OrganizerTests
@ -32,16 +34,26 @@ namespace NzbDrone.Core.Test.OrganizerTests
[TestCase("30 Rock - S00E05 - Episode Title", 0, true, "Season {season}", @"C:\Test\30 Rock\MySpecials\30 Rock - S00E05 - Episode Title.mkv")]
public void CalculateFilePath_SeasonFolder_SingleNumber(string filename, int seasonNumber, bool useSeasonFolder, string seasonFolderFormat, string expectedPath)
{
var fakeEpisodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(s => s.Title = "Episode Title")
.With(s => s.SeasonNumber = seasonNumber)
.With(s => s.EpisodeNumber = 5)
.Build().ToList();
var fakeSeries = Builder<Series>.CreateNew()
.With(s => s.Title = "30 Rock")
.With(s => s.Path = @"C:\Test\30 Rock".AsOsAgnostic())
.With(s => s.SeasonFolder = useSeasonFolder)
.With(s => s.SeriesType = SeriesTypes.Standard)
.Build();
var fakeEpisodeFile = Builder<EpisodeFile>.CreateNew()
.With(s => s.SceneName = filename)
.Build();
namingConfig.SeasonFolderFormat = seasonFolderFormat;
namingConfig.SpecialsFolderFormat = "MySpecials";
Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
Subject.BuildFilePath(fakeEpisodes, fakeSeries, fakeEpisodeFile, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
}
[Test]
@ -51,15 +63,25 @@ namespace NzbDrone.Core.Test.OrganizerTests
var seasonNumber = 1;
var expectedPath = @"C:\Test\NCIS - Los Angeles\NCIS - Los Angeles Season 1\S01E05 - Episode Title.mkv";
var fakeEpisodes = Builder<Episode>.CreateListOfSize(1)
.All()
.With(s => s.Title = "Episode Title")
.With(s => s.SeasonNumber = seasonNumber)
.With(s => s.EpisodeNumber = 5)
.Build().ToList();
var fakeSeries = Builder<Series>.CreateNew()
.With(s => s.Title = "NCIS: Los Angeles")
.With(s => s.Path = @"C:\Test\NCIS - Los Angeles".AsOsAgnostic())
.With(s => s.SeasonFolder = true)
.With(s => s.SeriesType = SeriesTypes.Standard)
.Build();
var fakeEpisodeFile = Builder<EpisodeFile>.CreateNew()
.With(s => s.SceneName = filename)
.Build();
namingConfig.SeasonFolderFormat = "{Series Title} Season {season:0}";
Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
Subject.BuildFilePath(fakeEpisodes, fakeSeries, fakeEpisodeFile, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
}
}
}

@ -93,6 +93,23 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_episodeFile.Quality.Revision.Version = 2;
}
[Test]
public void should_truncate_with_extension()
{
_series.Title = "The Fantastic Life of Mr. Sisko";
_episodes[0].SeasonNumber = 2;
_episodes[0].EpisodeNumber = 18;
_episodes[0].Title = "This title has to be 197 characters in length, combined with the series title, quality and episode number it becomes 254ish and the extension puts it above the 255 limit and triggers the truncation";
_episodeFile.Quality.Quality = Quality.Bluray1080p;
_episodes = _episodes.Take(1).ToList();
_namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}";
var result = Subject.BuildFileName(_episodes, _series, _episodeFile, ".mkv");
result.Length.Should().BeLessOrEqualTo(255);
result.Should().Be("The Fantastic Life of Mr. Sisko - S02E18 - This title has to be 197 characters in length, combined with the series title, quality and episode number it becomes 254ish and the extension puts it above the 255 limit and triggers the trunc... Bluray-1080p.mkv");
}
[Test]
public void should_truncate_with_ellipsis_between_first_and_last_episode_titles()
{

@ -59,8 +59,7 @@ namespace NzbDrone.Core.MediaFiles
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, Series series)
{
var episodes = _episodeService.GetEpisodesByFileId(episodeFile.Id);
var newFileName = _buildFileNames.BuildFileName(episodes, series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(series, episodes.First().SeasonNumber, newFileName, Path.GetExtension(episodeFile.RelativePath));
var filePath = _buildFileNames.BuildFilePath(episodes, series, episodeFile, Path.GetExtension(episodeFile.RelativePath));
EnsureEpisodeFolder(episodeFile, series, episodes.Select(v => v.SeasonNumber).First(), filePath);
@ -71,8 +70,7 @@ namespace NzbDrone.Core.MediaFiles
public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
{
var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path));
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path));
EnsureEpisodeFolder(episodeFile, localEpisode, filePath);
@ -83,8 +81,7 @@ namespace NzbDrone.Core.MediaFiles
public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode)
{
var newFileName = _buildFileNames.BuildFileName(localEpisode.Episodes, localEpisode.Series, episodeFile);
var filePath = _buildFileNames.BuildFilePath(localEpisode.Series, localEpisode.SeasonNumber, newFileName, Path.GetExtension(localEpisode.Path));
var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path));
EnsureEpisodeFolder(episodeFile, localEpisode, filePath);

@ -90,8 +90,7 @@ namespace NzbDrone.Core.MediaFiles
}
var seasonNumber = episodesInFile.First().SeasonNumber;
var newName = _filenameBuilder.BuildFileName(episodesInFile, series, file);
var newPath = _filenameBuilder.BuildFilePath(series, seasonNumber, newName, Path.GetExtension(episodeFilePath));
var newPath = _filenameBuilder.BuildFilePath(episodesInFile, series, file, Path.GetExtension(episodeFilePath));
if (!episodeFilePath.PathEquals(newPath, StringComparison.Ordinal))
{

@ -4,9 +4,12 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using FluentMigrator.Builders.Create.Column;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.MediaInfo;
@ -18,8 +21,8 @@ namespace NzbDrone.Core.Organizer
{
public interface IBuildFileNames
{
string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null, List<string> preferredWords = null);
string BuildFilePath(Series series, int seasonNumber, string fileName, string extension);
string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension = "", NamingConfig namingConfig = null, List<string> preferredWords = null);
string BuildFilePath(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension, NamingConfig namingConfig = null, List<string> preferredWords = null);
string BuildSeasonPath(Series series, int seasonNumber);
BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec);
string GetSeriesFolder(Series series, NamingConfig namingConfig = null);
@ -96,7 +99,7 @@ namespace NzbDrone.Core.Organizer
_logger = logger;
}
public string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null, List<string> preferredWords = null)
private string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension, int maxPath, NamingConfig namingConfig = null, List<string> preferredWords = null)
{
if (namingConfig == null)
{
@ -105,7 +108,7 @@ namespace NzbDrone.Core.Organizer
if (!namingConfig.RenameEpisodes)
{
return GetOriginalTitle(episodeFile);
return GetOriginalTitle(episodeFile) + extension;
}
if (namingConfig.StandardEpisodeFormat.IsNullOrWhiteSpace() && series.SeriesType == SeriesTypes.Standard)
@ -140,9 +143,9 @@ namespace NzbDrone.Core.Organizer
var splitPatterns = pattern.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
var components = new List<string>();
foreach (var s in splitPatterns)
for (var i = 0; i < splitPatterns.Length; i++)
{
var splitPattern = s;
var splitPattern = splitPatterns[i];
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
splitPattern = AddSeasonEpisodeNumberingTokens(splitPattern, tokenHandlers, episodes, namingConfig);
splitPattern = AddAbsoluteNumberingTokens(splitPattern, tokenHandlers, series, episodes, namingConfig);
@ -159,7 +162,12 @@ namespace NzbDrone.Core.Organizer
AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords);
var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig, true).Trim();
var maxEpisodeTitleLength = 255 - GetLengthWithoutEpisodeTitle(component, namingConfig);
var maxPathSegmentLength = LongPathSupport.MaxFileNameLength;
if (i == splitPatterns.Length - 1)
{
maxPathSegmentLength -= extension.Length;
}
var maxEpisodeTitleLength = maxPathSegmentLength - GetLengthWithoutEpisodeTitle(component, namingConfig);
AddEpisodeTitleTokens(tokenHandlers, episodes, maxEpisodeTitleLength);
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
@ -171,18 +179,25 @@ namespace NzbDrone.Core.Organizer
components.Add(component);
}
return string.Join(Path.DirectorySeparatorChar.ToString(), components);
return string.Join(Path.DirectorySeparatorChar.ToString(), components) + extension;
}
public string BuildFilePath(Series series, int seasonNumber, string fileName, string extension)
public string BuildFileName(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension = "", NamingConfig namingConfig = null, List<string> preferredWords = null)
{
Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace();
return BuildFileName(episodes, series, episodeFile, extension, LongPathSupport.MaxFilePathLength, namingConfig, preferredWords);
}
var path = BuildSeasonPath(series, seasonNumber);
public string BuildFilePath(List<Episode> episodes, Series series, EpisodeFile episodeFile, string extension, NamingConfig namingConfig = null, List<string> preferredWords = null)
{
Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace();
var seasonPath = BuildSeasonPath(series, episodes.First().SeasonNumber);
var remainingPathLength = LongPathSupport.MaxFilePathLength - seasonPath.Length - 1;
var fileName = BuildFileName(episodes, series, episodeFile, extension, remainingPathLength, namingConfig, preferredWords);
return Path.Combine(path, fileName + extension);
return Path.Combine(seasonPath, fileName);
}
public string BuildSeasonPath(Series series, int seasonNumber)
{
var path = series.Path;

@ -259,7 +259,7 @@ namespace NzbDrone.Core.Organizer
{
try
{
return _buildFileNames.BuildFileName(episodes, series, episodeFile, nameSpec, _preferredWords);
return _buildFileNames.BuildFileName(episodes, series, episodeFile, "", nameSpec, _preferredWords);
}
catch (NamingFormatException)
{

Loading…
Cancel
Save