From 158e31d54aab2419e79b3a7b2f3ef918954a440b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 14 Nov 2020 21:20:03 +0100 Subject: [PATCH] Fixed: Truncating too long filenames with unicode characters closes #4085 --- .../TruncatedEpisodeTitlesFixture.cs | 40 +++++++++++++++++++ src/NzbDrone.Core/Fluent.cs | 5 +++ .../Organizer/FileNameBuilder.cs | 24 ++++++----- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedEpisodeTitlesFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedEpisodeTitlesFixture.cs index 1e73c0e95..cb4caa445 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedEpisodeTitlesFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/TruncatedEpisodeTitlesFixture.cs @@ -141,5 +141,45 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests result.Length.Should().BeLessOrEqualTo(255); result.Should().Be("Lorem ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum - S01E01 - Episode Ti... HDTV-720p"); } + + [Test] + public void should_truncate_titles_measuring_series_title_bytes() + { + _series.Title = "Lor\u00E9m ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}"; + + var result = Subject.BuildFileName(new List { _episodes.First() }, _series, _episodeFile); + result.GetByteCount().Should().BeLessOrEqualTo(255); + + result.Should().Be("Lor\u00E9m ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum - S01E01 - Episode T... HDTV-720p"); + } + + [Test] + public void should_truncate_titles_measuring_episode_title_bytes() + { + _series.Title = "Lorem ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}"; + + _episodes.First().Title = "Episod\u00E9 Title"; + + var result = Subject.BuildFileName(new List { _episodes.First() }, _series, _episodeFile); + result.GetByteCount().Should().BeLessOrEqualTo(255); + + result.Should().Be("Lorem ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum - S01E01 - Episod\u00E9 T... HDTV-720p"); + } + + [Test] + public void should_truncate_titles_measuring_episode_title_bytes_middle() + { + _series.Title = "Lorem ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum"; + _namingConfig.StandardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}"; + + _episodes.First().Title = "Episode T\u00E9tle"; + + var result = Subject.BuildFileName(new List { _episodes.First() }, _series, _episodeFile); + result.GetByteCount().Should().BeLessOrEqualTo(255); + + result.Should().Be("Lorem ipsum dolor sit amet, consectetur adipiscing elit Maecenas et magna sem Morbi vitae volutpat quam, id porta arcu Orci varius natoque penatibus et magnis dis parturient montes nascetur ridiculus musu Cras vestibulum - S01E01 - Episode T... HDTV-720p"); + } } } diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 6e2e3d2b2..4e8de9c24 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -76,6 +76,11 @@ namespace NzbDrone.Core return intList.Max(); } + public static int GetByteCount(this string input) + { + return Encoding.UTF8.GetByteCount(input); + } + public static string Truncate(this string s, int maxLength) { if (Encoding.UTF8.GetByteCount(s) <= maxLength) diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 56584d546..63a6305d6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -163,10 +163,10 @@ namespace NzbDrone.Core.Organizer AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords); var component = ReplaceTokens(splitPattern, tokenHandlers, namingConfig, true).Trim(); - var maxPathSegmentLength = LongPathSupport.MaxFileNameLength; + var maxPathSegmentLength = Math.Min(LongPathSupport.MaxFileNameLength, maxPath); if (i == splitPatterns.Length - 1) { - maxPathSegmentLength -= extension.Length; + maxPathSegmentLength -= extension.GetByteCount(); } var maxEpisodeTitleLength = maxPathSegmentLength - GetLengthWithoutEpisodeTitle(component, namingConfig); @@ -193,7 +193,7 @@ namespace NzbDrone.Core.Organizer Ensure.That(extension, () => extension).IsNotNullOrWhiteSpace(); var seasonPath = BuildSeasonPath(series, episodes.First().SeasonNumber); - var remainingPathLength = LongPathSupport.MaxFilePathLength - seasonPath.Length - 1; + var remainingPathLength = LongPathSupport.MaxFilePathLength - seasonPath.GetByteCount() - 1; var fileName = BuildFileName(episodes, series, episodeFile, extension, remainingPathLength, namingConfig, preferredWords); return Path.Combine(seasonPath, fileName); @@ -935,33 +935,35 @@ namespace NzbDrone.Core.Organizer var joined = string.Join(separator, titles); - if (joined.Length <= maxLength) + if (joined.GetByteCount() <= maxLength) { return joined; } var firstTitle = titles.First(); + var firstTitleLength = firstTitle.GetByteCount(); if (titles.Count >= 2) { var lastTitle = titles.Last(); - if (firstTitle.Length + lastTitle.Length + 3 <= maxLength) + var lastTitleLength = lastTitle.GetByteCount(); + if (firstTitleLength + lastTitleLength + 3 <= maxLength) { - return $"{firstTitle.Trim(' ', '.')}{{ellipsis}}{lastTitle}"; + return $"{firstTitle.TrimEnd(' ', '.')}{{ellipsis}}{lastTitle}"; } } - if (titles.Count > 1 && firstTitle.Length + 3 <= maxLength) + if (titles.Count > 1 && firstTitleLength + 3 <= maxLength) { - return $"{firstTitle.Trim(' ', '.')}{{ellipsis}}"; + return $"{firstTitle.TrimEnd(' ', '.')}{{ellipsis}}"; } - if (titles.Count == 1 && firstTitle.Length <= maxLength) + if (titles.Count == 1 && firstTitleLength <= maxLength) { return firstTitle; } - return $"{firstTitle.Substring(0, maxLength - 3).Trim(' ', '.')}{{ellipsis}}"; + return $"{firstTitle.Truncate(maxLength - 3).TrimEnd(' ', '.')}{{ellipsis}}"; } private string CleanupEpisodeTitle(string title) @@ -1023,7 +1025,7 @@ namespace NzbDrone.Core.Organizer var result = ReplaceTokens(pattern, tokenHandlers, namingConfig); - return result.Length; + return result.GetByteCount(); } }