New: Add additional CleanNameThe/CleanTitleThe naming tokens

(cherry picked from commit 81aaf00a4cd2b3a2f8ddf67226c13bb51ea39dda)

Add some translations and fix the validation for track naming

Closes #4197
pull/4547/head
Stevie Robinson 1 year ago committed by Bogdan
parent 2f80957f11
commit 0121095b3e

@ -15,16 +15,51 @@ import NamingOption from './NamingOption';
import styles from './NamingModal.css'; import styles from './NamingModal.css';
const separatorOptions = [ const separatorOptions = [
{ key: ' ', value: 'Space ( )' }, {
{ key: '.', value: 'Period (.)' }, key: ' ',
{ key: '_', value: 'Underscore (_)' }, get value() {
{ key: '-', value: 'Dash (-)' } return `${translate('Space')} ( )`;
}
},
{
key: '.',
get value() {
return `${translate('Period')} (.)`;
}
},
{
key: '_',
get value() {
return `${translate('Underscore')} (_)`;
}
},
{
key: '-',
get value() {
return `${translate('Dash')} (-)`;
}
}
]; ];
const caseOptions = [ const caseOptions = [
{ key: 'title', value: 'Default Case' }, {
{ key: 'lower', value: 'Lowercase' }, key: 'title',
{ key: 'upper', value: 'Uppercase' } get value() {
return translate('DefaultCase');
}
},
{
key: 'lower',
get value() {
return translate('Lowercase');
}
},
{
key: 'upper',
get value() {
return translate('Uppercase');
}
}
]; ];
const fileNameTokens = [ const fileNameTokens = [
@ -40,33 +75,23 @@ const fileNameTokens = [
const artistTokens = [ const artistTokens = [
{ token: '{Artist Name}', example: 'Artist Name' }, { token: '{Artist Name}', example: 'Artist Name' },
{ token: '{Artist CleanName}', example: 'Artist Name' },
{ token: '{Artist NameThe}', example: 'Artist Name, The' }, { token: '{Artist NameThe}', example: 'Artist Name, The' },
{ token: '{Artist CleanNameThe}', example: 'Artist Name, The' },
{ token: '{Artist NameFirstCharacter}', example: 'A' }, { token: '{Artist NameFirstCharacter}', example: 'A' },
{ token: '{Artist CleanName}', example: 'Artist Name' },
{ token: '{Artist Disambiguation}', example: 'Disambiguation' }, { token: '{Artist Disambiguation}', example: 'Disambiguation' },
{ token: '{Artist Genre}', example: 'Pop' }, { token: '{Artist Genre}', example: 'Pop' },
{ token: '{Artist MbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' } { token: '{Artist MbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' }
]; ];
const albumTokens = [ const albumTokens = [
{ token: '{Album Title}', example: 'Album Title' }, { token: '{Album Title}', example: 'Album Title' },
{ token: '{Album TitleThe}', example: 'Album Title, The' },
{ token: '{Album CleanTitle}', example: 'Album Title' }, { 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 Type}', example: 'Album Type' },
{ token: '{Album Disambiguation}', example: 'Disambiguation' }, { token: '{Album Disambiguation}', example: 'Disambiguation' },
{ token: '{Album Genre}', example: 'Rock' }, { token: '{Album Genre}', example: 'Rock' },
{ token: '{Album MbId}', example: '082c6aff-a7cc-36e0-a960-35a578ecd937' } { token: '{Album MbId}', example: '082c6aff-a7cc-36e0-a960-35a578ecd937' }
]; ];
@ -96,8 +121,9 @@ const trackTitleTokens = [
const trackArtistTokens = [ const trackArtistTokens = [
{ token: '{Track ArtistName}', example: 'Artist Name' }, { token: '{Track ArtistName}', example: 'Artist Name' },
{ token: '{Track ArtistNameThe}', example: 'Artist Name, The' },
{ token: '{Track ArtistCleanName}', example: 'Artist Name' }, { 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' } { token: '{Track ArtistMbId}', example: 'db92a151-1ac2-438b-bc43-b82e149ddd50' }
]; ];
@ -213,7 +239,7 @@ class NamingModal extends Component {
> >
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
File Name Tokens {translate('FileNameTokens')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@ -552,7 +578,7 @@ class NamingModal extends Component {
onSelectionChange={this.onInputSelectionChange} onSelectionChange={this.onInputSelectionChange}
/> />
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
Close {translate('Close')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

@ -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<FileNameBuilder>
{
private Artist _artist;
private Album _album;
private AlbumRelease _release;
private Track _track;
private TrackFile _trackFile;
private NamingConfig _namingConfig;
[SetUp]
public void Setup()
{
_artist = Builder<Artist>
.CreateNew()
.With(s => s.Name = "Avenged Sevenfold")
.Build();
_album = Builder<Album>
.CreateNew()
.With(s => s.Title = "Hail to the King")
.Build();
_release = Builder<AlbumRelease>
.CreateNew()
.With(s => s.Media = new List<Medium> { new Medium { Number = 1 } })
.Build();
_track = Builder<Track>.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<INamingConfigService>()
.Setup(c => c.GetConfig()).Returns(_namingConfig);
Mocker.GetMock<IQualityDefinitionService>()
.Setup(v => v.Get(Moq.It.IsAny<Quality>()))
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
Mocker.GetMock<ICustomFormatService>()
.Setup(v => v.All())
.Returns(new List<CustomFormat>());
}
[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> { _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> { _track }, _artist, _album, _trackFile)
.Should().Be(title);
}
}
}

@ -256,6 +256,7 @@
"CutoffFormatScoreHelpText": "Once this custom format score is reached {appName} will no longer grab album releases", "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", "CutoffHelpText": "Once this quality is reached {appName} will no longer download albums",
"CutoffUnmet": "Cutoff Unmet", "CutoffUnmet": "Cutoff Unmet",
"Dash": "Dash",
"DashOrSpaceDashDependingOnName": "Dash or Space Dash depending on name", "DashOrSpaceDashDependingOnName": "Dash or Space Dash depending on name",
"Database": "Database", "Database": "Database",
"DatabaseMigration": "Database Migration", "DatabaseMigration": "Database Migration",
@ -263,6 +264,7 @@
"DateAdded": "Date Added", "DateAdded": "Date Added",
"Dates": "Dates", "Dates": "Dates",
"Deceased": "Deceased", "Deceased": "Deceased",
"DefaultCase": "Default Case",
"DefaultDelayProfileHelpText": "This is the default profile. It applies to all artist that don't have an explicit profile.", "DefaultDelayProfileHelpText": "This is the default profile. It applies to all artist that don't have an explicit profile.",
"DefaultLidarrTags": "Default {appName} Tags", "DefaultLidarrTags": "Default {appName} Tags",
"DefaultMetadataProfileIdHelpText": "Default Metadata Profile for artists detected in this folder", "DefaultMetadataProfileIdHelpText": "Default Metadata Profile for artists detected in this folder",
@ -449,6 +451,7 @@
"FailedToLoadQueue": "Failed to load Queue", "FailedToLoadQueue": "Failed to load Queue",
"FileDateHelpText": "Change file date on import/rescan", "FileDateHelpText": "Change file date on import/rescan",
"FileManagement": "File Management", "FileManagement": "File Management",
"FileNameTokens": "File Name Tokens",
"FileNames": "File Names", "FileNames": "File Names",
"Filename": "Filename", "Filename": "Filename",
"Files": "Files", "Files": "Files",
@ -614,6 +617,7 @@
"Logout": "Logout", "Logout": "Logout",
"Logs": "Logs", "Logs": "Logs",
"LongDateFormat": "Long Date Format", "LongDateFormat": "Long Date Format",
"Lowercase": "Lowercase",
"MIA": "MIA", "MIA": "MIA",
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details", "MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
"ManageClients": "Manage Clients", "ManageClients": "Manage Clients",
@ -764,6 +768,7 @@
"PathHelpText": "Root Folder containing your music library", "PathHelpText": "Root Folder containing your music library",
"PathHelpTextWarning": "This must be different to the directory where your download client puts files", "PathHelpTextWarning": "This must be different to the directory where your download client puts files",
"Peers": "Peers", "Peers": "Peers",
"Period": "Period",
"Permissions": "Permissions", "Permissions": "Permissions",
"Playlist": "Playlist", "Playlist": "Playlist",
"Port": "Port", "Port": "Port",
@ -1028,6 +1033,7 @@
"Source": "Source", "Source": "Source",
"SourcePath": "Source Path", "SourcePath": "Source Path",
"SourceTitle": "Source Title", "SourceTitle": "Source Title",
"Space": "Space",
"SpecificAlbum": "Specific Album", "SpecificAlbum": "Specific Album",
"SpecificMonitoringOptionHelpText": "Monitor artists but only monitor albums explicitly included in the list", "SpecificMonitoringOptionHelpText": "Monitor artists but only monitor albums explicitly included in the list",
"SslCertPasswordHelpText": "Password for pfx file", "SslCertPasswordHelpText": "Password for pfx file",
@ -1147,6 +1153,7 @@
"UnableToLoadTags": "Unable to load Tags", "UnableToLoadTags": "Unable to load Tags",
"UnableToLoadTheCalendar": "Unable to load the calendar", "UnableToLoadTheCalendar": "Unable to load the calendar",
"UnableToLoadUISettings": "Unable to load UI settings", "UnableToLoadUISettings": "Unable to load UI settings",
"Underscore": "Underscore",
"Ungroup": "Ungroup", "Ungroup": "Ungroup",
"Unlimited": "Unlimited", "Unlimited": "Unlimited",
"UnmappedFiles": "Unmapped Files", "UnmappedFiles": "Unmapped Files",
@ -1169,6 +1176,7 @@
"UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead": "Updating is disabled inside a docker container. Update the container image instead.", "UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead": "Updating is disabled inside a docker container. Update the container image instead.",
"UpgradeAllowedHelpText": "If disabled qualities will not be upgraded", "UpgradeAllowedHelpText": "If disabled qualities will not be upgraded",
"UpgradesAllowed": "Upgrades Allowed", "UpgradesAllowed": "Upgrades Allowed",
"Uppercase": "Uppercase",
"Uptime": "Uptime", "Uptime": "Uptime",
"UrlBaseHelpText": "For reverse proxy support, default is empty", "UrlBaseHelpText": "For reverse proxy support, default is empty",
"UrlBaseHelpTextWarning": "Requires restart to take effect", "UrlBaseHelpTextWarning": "Requires restart to take effect",

@ -54,7 +54,7 @@ namespace NzbDrone.Core.Organizer
public static readonly Regex AlbumTitleRegex = new Regex(@"(?<token>\{(?:Album)(?<separator>[- ._])(Clean)?Title(The)?(?::(?<customFormat>[0-9-]+))?\})", public static readonly Regex AlbumTitleRegex = new Regex(@"(?<token>\{(?:Album)(?<separator>[- ._])(Clean)?Title(The)?(?::(?<customFormat>[0-9-]+))?\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly Regex TrackTitleRegex = new Regex(@"(?<token>\{(?:Track)(?<separator>[- ._])(Clean)?Title(The)?(?::(?<customFormat>[0-9-]+))?\})", public static readonly Regex TrackTitleRegex = new Regex(@"(?<token>\{(?:Track)(?<separator>[- ._])(Clean)?Title(?::(?<customFormat>[0-9-]+))?\})",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); 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"); 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) public static string TitleFirstCharacter(string title)
{ {
if (char.IsLetterOrDigit(title[0])) if (char.IsLetterOrDigit(title[0]))
@ -300,6 +311,7 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Artist Name}"] = m => Truncate(artist.Name, m.CustomFormat); tokenHandlers["{Artist Name}"] = m => Truncate(artist.Name, m.CustomFormat);
tokenHandlers["{Artist CleanName}"] = m => Truncate(CleanTitle(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 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 Genre}"] = m => artist.Metadata.Value.Genres?.FirstOrDefault() ?? string.Empty;
tokenHandlers["{Artist NameFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(artist.Name)); tokenHandlers["{Artist NameFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(artist.Name));
tokenHandlers["{Artist MbId}"] = m => artist.ForeignArtistId ?? string.Empty; 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 Title}"] = m => Truncate(album.Title, m.CustomFormat);
tokenHandlers["{Album CleanTitle}"] = m => Truncate(CleanTitle(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 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 Type}"] = m => album.AlbumType;
tokenHandlers["{Album Genre}"] = m => album.Genres.FirstOrDefault() ?? string.Empty; tokenHandlers["{Album Genre}"] = m => album.Genres.FirstOrDefault() ?? string.Empty;
tokenHandlers["{Album MbId}"] = m => album.ForeignAlbumId ?? 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 ArtistName}"] = m => Truncate(firstArtist.Name, m.CustomFormat);
tokenHandlers["{Track ArtistCleanName}"] = m => Truncate(CleanTitle(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 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; tokenHandlers["{Track ArtistMbId}"] = m => firstArtist.ForeignArtistId ?? string.Empty;
} }
} }

Loading…
Cancel
Save