From 0121095b3e4761f4f91eed7bf94932d71ce7a874 Mon Sep 17 00:00:00 2001 From: Stevie Robinson Date: Tue, 10 Oct 2023 15:58:03 +0200 Subject: [PATCH] New: Add additional CleanNameThe/CleanTitleThe naming tokens (cherry picked from commit 81aaf00a4cd2b3a2f8ddf67226c13bb51ea39dda) Add some translations and fix the validation for track naming Closes #4197 --- .../MediaManagement/Naming/NamingModal.js | 74 +++++++++----- .../CleanTitleTheFixture.cs | 98 +++++++++++++++++++ src/NzbDrone.Core/Localization/Core/en.json | 8 ++ .../Organizer/FileNameBuilder.cs | 16 ++- 4 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 35244e64e..dec15893f 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -15,16 +15,51 @@ import NamingOption from './NamingOption'; import styles from './NamingModal.css'; const separatorOptions = [ - { key: ' ', value: 'Space ( )' }, - { key: '.', value: 'Period (.)' }, - { key: '_', value: 'Underscore (_)' }, - { key: '-', value: 'Dash (-)' } + { + key: ' ', + get value() { + return `${translate('Space')} ( )`; + } + }, + { + key: '.', + get value() { + return `${translate('Period')} (.)`; + } + }, + { + key: '_', + get value() { + return `${translate('Underscore')} (_)`; + } + }, + { + key: '-', + get value() { + return `${translate('Dash')} (-)`; + } + } ]; const caseOptions = [ - { key: 'title', value: 'Default Case' }, - { key: 'lower', value: 'Lowercase' }, - { key: 'upper', value: 'Uppercase' } + { + key: 'title', + get value() { + return translate('DefaultCase'); + } + }, + { + key: 'lower', + get value() { + return translate('Lowercase'); + } + }, + { + key: 'upper', + get value() { + return translate('Uppercase'); + } + } ]; const fileNameTokens = [ @@ -40,33 +75,23 @@ const fileNameTokens = [ const artistTokens = [ { token: '{Artist Name}', example: 'Artist Name' }, - + { token: '{Artist CleanName}', example: 'Artist Name' }, { token: '{Artist NameThe}', example: 'Artist Name, The' }, - + { token: '{Artist CleanNameThe}', example: 'Artist Name, The' }, { token: '{Artist NameFirstCharacter}', example: 'A' }, - - { token: '{Artist CleanName}', example: 'Artist Name' }, - { token: '{Artist Disambiguation}', example: 'Disambiguation' }, - { token: '{Artist Genre}', example: 'Pop' }, - { token: '{Artist MbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; const albumTokens = [ { token: '{Album Title}', example: 'Album Title' }, - - { token: '{Album TitleThe}', example: 'Album Title, The' }, - { token: '{Album CleanTitle}', example: 'Album Title' }, - + { token: '{Album TitleThe}', example: 'Album Title, The' }, + { token: '{Album CleanTitleThe}', example: 'Album Title, The' }, { token: '{Album Type}', example: 'Album Type' }, - { token: '{Album Disambiguation}', example: 'Disambiguation' }, - { token: '{Album Genre}', example: 'Rock' }, - { token: '{Album MbId}', example: '082c6aff-a7cc-36e0-a960-35a578ecd937' } ]; @@ -96,8 +121,9 @@ const trackTitleTokens = [ const trackArtistTokens = [ { token: '{Track ArtistName}', example: 'Artist Name' }, - { token: '{Track ArtistNameThe}', example: 'Artist Name, The' }, { token: '{Track ArtistCleanName}', example: 'Artist Name' }, + { token: '{Track ArtistNameThe}', example: 'Artist Name, The' }, + { token: '{Track ArtistCleanNameThe}', example: 'Artist Name, The' }, { token: '{Track ArtistMbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } ]; @@ -213,7 +239,7 @@ class NamingModal extends Component { > - File Name Tokens + {translate('FileNameTokens')} @@ -552,7 +578,7 @@ class NamingModal extends Component { onSelectionChange={this.onInputSelectionChange} /> diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs new file mode 100644 index 000000000..3cbfd9928 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Music; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class CleanTitleTheFixture : CoreTest + { + private Artist _artist; + private Album _album; + private AlbumRelease _release; + private Track _track; + private TrackFile _trackFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _artist = Builder + .CreateNew() + .With(s => s.Name = "Avenged Sevenfold") + .Build(); + + _album = Builder + .CreateNew() + .With(s => s.Title = "Hail to the King") + .Build(); + + _release = Builder + .CreateNew() + .With(s => s.Media = new List { new Medium { Number = 1 } }) + .Build(); + + _track = Builder.CreateNew() + .With(e => e.Title = "Doing Time") + .With(e => e.AbsoluteTrackNumber = 3) + .With(e => e.AlbumRelease = _release) + .Build(); + + _trackFile = new TrackFile { Quality = new QualityModel(Quality.MP3_256), ReleaseGroup = "LidarrTest" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameTracks = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); + } + + [TestCase("The Mist", "Mist, The")] + [TestCase("A Place to Call Home", "Place to Call Home, A")] + [TestCase("An Adventure in Space and Time", "Adventure in Space and Time, An")] + [TestCase("The Flash (2010)", "Flash, The 2010")] + [TestCase("A League Of Their Own (AU)", "League Of Their Own, A AU")] + [TestCase("The Fixer (ZH) (2015)", "Fixer, The ZH 2015")] + [TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The Thai")] + [TestCase("The Amazing Race (Latin America)", "Amazing Race, The Latin America")] + [TestCase("The Rat Pack (A&E)", "Rat Pack, The AandE")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax I Almost Got Away With It, The 2016")] + public void should_get_expected_title_back(string title, string expected) + { + _artist.Name = title; + _namingConfig.StandardTrackFormat = "{Artist CleanNameThe}"; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(expected); + } + + [TestCase("A")] + [TestCase("Anne")] + [TestCase("Theodore")] + [TestCase("3%")] + public void should_not_change_title(string title) + { + _artist.Name = title; + _namingConfig.StandardTrackFormat = "{Artist CleanNameThe}"; + + Subject.BuildTrackFileName(new List { _track }, _artist, _album, _trackFile) + .Should().Be(title); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 15e3aa631..c08cf304d 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -256,6 +256,7 @@ "CutoffFormatScoreHelpText": "Once this custom format score is reached {appName} will no longer grab album releases", "CutoffHelpText": "Once this quality is reached {appName} will no longer download albums", "CutoffUnmet": "Cutoff Unmet", + "Dash": "Dash", "DashOrSpaceDashDependingOnName": "Dash or Space Dash depending on name", "Database": "Database", "DatabaseMigration": "Database Migration", @@ -263,6 +264,7 @@ "DateAdded": "Date Added", "Dates": "Dates", "Deceased": "Deceased", + "DefaultCase": "Default Case", "DefaultDelayProfileHelpText": "This is the default profile. It applies to all artist that don't have an explicit profile.", "DefaultLidarrTags": "Default {appName} Tags", "DefaultMetadataProfileIdHelpText": "Default Metadata Profile for artists detected in this folder", @@ -449,6 +451,7 @@ "FailedToLoadQueue": "Failed to load Queue", "FileDateHelpText": "Change file date on import/rescan", "FileManagement": "File Management", + "FileNameTokens": "File Name Tokens", "FileNames": "File Names", "Filename": "Filename", "Files": "Files", @@ -614,6 +617,7 @@ "Logout": "Logout", "Logs": "Logs", "LongDateFormat": "Long Date Format", + "Lowercase": "Lowercase", "MIA": "MIA", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "ManageClients": "Manage Clients", @@ -764,6 +768,7 @@ "PathHelpText": "Root Folder containing your music library", "PathHelpTextWarning": "This must be different to the directory where your download client puts files", "Peers": "Peers", + "Period": "Period", "Permissions": "Permissions", "Playlist": "Playlist", "Port": "Port", @@ -1028,6 +1033,7 @@ "Source": "Source", "SourcePath": "Source Path", "SourceTitle": "Source Title", + "Space": "Space", "SpecificAlbum": "Specific Album", "SpecificMonitoringOptionHelpText": "Monitor artists but only monitor albums explicitly included in the list", "SslCertPasswordHelpText": "Password for pfx file", @@ -1147,6 +1153,7 @@ "UnableToLoadTags": "Unable to load Tags", "UnableToLoadTheCalendar": "Unable to load the calendar", "UnableToLoadUISettings": "Unable to load UI settings", + "Underscore": "Underscore", "Ungroup": "Ungroup", "Unlimited": "Unlimited", "UnmappedFiles": "Unmapped Files", @@ -1169,6 +1176,7 @@ "UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead": "Updating is disabled inside a docker container. Update the container image instead.", "UpgradeAllowedHelpText": "If disabled qualities will not be upgraded", "UpgradesAllowed": "Upgrades Allowed", + "Uppercase": "Uppercase", "Uptime": "Uptime", "UrlBaseHelpText": "For reverse proxy support, default is empty", "UrlBaseHelpTextWarning": "Requires restart to take effect", diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index cf6007da5..c8cb22a64 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -54,7 +54,7 @@ namespace NzbDrone.Core.Organizer public static readonly Regex AlbumTitleRegex = new Regex(@"(?\{(?:Album)(?[- ._])(Clean)?Title(The)?(?::(?[0-9-]+))?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public static readonly Regex TrackTitleRegex = new Regex(@"(?\{(?:Track)(?[- ._])(Clean)?Title(The)?(?::(?[0-9-]+))?\})", + public static readonly Regex TrackTitleRegex = new Regex(@"(?\{(?:Track)(?[- ._])(Clean)?Title(?::(?[0-9-]+))?\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); @@ -266,6 +266,17 @@ namespace NzbDrone.Core.Organizer return TitlePrefixRegex.Replace(title, "$2, $1$3"); } + public static string CleanTitleThe(string title) + { + if (TitlePrefixRegex.IsMatch(title)) + { + var splitResult = TitlePrefixRegex.Split(title); + return $"{CleanTitle(splitResult[2]).Trim()}, {splitResult[1]}{CleanTitle(splitResult[3])}"; + } + + return CleanTitle(title); + } + public static string TitleFirstCharacter(string title) { if (char.IsLetterOrDigit(title[0])) @@ -300,6 +311,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Artist Name}"] = m => Truncate(artist.Name, m.CustomFormat); tokenHandlers["{Artist CleanName}"] = m => Truncate(CleanTitle(artist.Name), m.CustomFormat); tokenHandlers["{Artist NameThe}"] = m => Truncate(TitleThe(artist.Name), m.CustomFormat); + tokenHandlers["{Artist CleanNameThe}"] = m => Truncate(CleanTitleThe(artist.Name), m.CustomFormat); tokenHandlers["{Artist Genre}"] = m => artist.Metadata.Value.Genres?.FirstOrDefault() ?? string.Empty; tokenHandlers["{Artist NameFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(artist.Name)); tokenHandlers["{Artist MbId}"] = m => artist.ForeignArtistId ?? string.Empty; @@ -315,6 +327,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Album Title}"] = m => Truncate(album.Title, m.CustomFormat); tokenHandlers["{Album CleanTitle}"] = m => Truncate(CleanTitle(album.Title), m.CustomFormat); tokenHandlers["{Album TitleThe}"] = m => Truncate(TitleThe(album.Title), m.CustomFormat); + tokenHandlers["{Album CleanTitleThe}"] = m => Truncate(CleanTitleThe(album.Title), m.CustomFormat); tokenHandlers["{Album Type}"] = m => album.AlbumType; tokenHandlers["{Album Genre}"] = m => album.Genres.FirstOrDefault() ?? string.Empty; tokenHandlers["{Album MbId}"] = m => album.ForeignAlbumId ?? string.Empty; @@ -346,6 +359,7 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{Track ArtistName}"] = m => Truncate(firstArtist.Name, m.CustomFormat); tokenHandlers["{Track ArtistCleanName}"] = m => Truncate(CleanTitle(firstArtist.Name), m.CustomFormat); tokenHandlers["{Track ArtistNameThe}"] = m => Truncate(TitleThe(firstArtist.Name), m.CustomFormat); + tokenHandlers["{Track ArtistCleanNameThe}"] = m => Truncate(CleanTitleThe(firstArtist.Name), m.CustomFormat); tokenHandlers["{Track ArtistMbId}"] = m => firstArtist.ForeignArtistId ?? string.Empty; } }