mirror of
https://github.com/fergalmoran/Readarr.git
synced 2025-12-22 09:29:59 +00:00
New: Colon replacement naming option
(cherry picked from commit b3260ba8661f3b2c6996eee7e04974e8f41365d5)
This commit is contained in:
@@ -74,6 +74,15 @@ class Naming extends Component {
|
||||
} = this.state;
|
||||
|
||||
const renameBooks = hasSettings && settings.renameBooks.value;
|
||||
const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
|
||||
|
||||
const colonReplacementOptions = [
|
||||
{ key: 0, value: translate('Delete') },
|
||||
{ key: 1, value: translate('ReplaceWithDash') },
|
||||
{ key: 2, value: translate('ReplaceWithSpaceDash') },
|
||||
{ key: 3, value: translate('ReplaceWithSpaceDashSpace') },
|
||||
{ key: 4, value: translate('SmartReplace'), hint: translate('DashOrSpaceDashDependingOnName') }
|
||||
];
|
||||
|
||||
const standardBookFormatHelpTexts = [];
|
||||
const standardBookFormatErrors = [];
|
||||
@@ -145,6 +154,24 @@ class Naming extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
replaceIllegalCharacters ?
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ColonReplacement')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="colonReplacementFormat"
|
||||
values={colonReplacementOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.colonReplacementFormat}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
renameBooks &&
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Organizer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ColonReplacementFixture : CoreTest<FileNameBuilder>
|
||||
{
|
||||
private Author _author;
|
||||
private Book _book;
|
||||
private Edition _edition;
|
||||
private BookFile _bookFile;
|
||||
private NamingConfig _namingConfig;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_author = Builder<Author>
|
||||
.CreateNew()
|
||||
.With(s => s.Name = "Christopher Hopper")
|
||||
.Build();
|
||||
|
||||
var series = Builder<Series>
|
||||
.CreateNew()
|
||||
.With(x => x.Title = "Series: Ruins of the Earth")
|
||||
.Build();
|
||||
|
||||
var seriesLink = Builder<SeriesBookLink>
|
||||
.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(s => s.Position = "1-2")
|
||||
.With(s => s.Series = series)
|
||||
.BuildListOfNew();
|
||||
|
||||
_book = Builder<Book>
|
||||
.CreateNew()
|
||||
.With(s => s.Title = "Fake: Phantom Deadfall")
|
||||
.With(s => s.AuthorMetadata = _author.Metadata.Value)
|
||||
.With(s => s.ReleaseDate = new DateTime(2021, 2, 14))
|
||||
.With(s => s.SeriesLinks = seriesLink)
|
||||
.Build();
|
||||
|
||||
_edition = Builder<Edition>
|
||||
.CreateNew()
|
||||
.With(s => s.Monitored = true)
|
||||
.With(s => s.Book = _book)
|
||||
.With(s => s.Title = _book.Title)
|
||||
.With(s => s.ReleaseDate = new DateTime(2021, 2, 17))
|
||||
.Build();
|
||||
|
||||
_bookFile = new BookFile { Quality = new QualityModel(Quality.EPUB), ReleaseGroup = "ReadarrTest" };
|
||||
|
||||
_namingConfig = NamingConfig.Default;
|
||||
_namingConfig.RenameBooks = 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>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_replace_colon_followed_by_space_with_space_dash_space_by_default()
|
||||
{
|
||||
_namingConfig.StandardBookFormat = "{Author Name} - {Book SeriesTitle - }{Book Title} {(Release Year)}";
|
||||
|
||||
Subject.BuildBookFileName(_author, _edition, _bookFile)
|
||||
.Should().Be("Christopher Hopper - Series - Ruins of the Earth #1-2 - Fake - Phantom Deadfall (2021)");
|
||||
}
|
||||
|
||||
[TestCase("Fake: Phantom Deadfall", ColonReplacementFormat.Smart, "Christopher Hopper - Series - Ruins of the Earth - Fake - Phantom Deadfall (2021)")]
|
||||
[TestCase("Fake: Phantom Deadfall", ColonReplacementFormat.Dash, "Christopher Hopper - Series- Ruins of the Earth - Fake- Phantom Deadfall (2021)")]
|
||||
[TestCase("Fake: Phantom Deadfall", ColonReplacementFormat.Delete, "Christopher Hopper - Series Ruins of the Earth - Fake Phantom Deadfall (2021)")]
|
||||
[TestCase("Fake: Phantom Deadfall", ColonReplacementFormat.SpaceDash, "Christopher Hopper - Series - Ruins of the Earth - Fake - Phantom Deadfall (2021)")]
|
||||
[TestCase("Fake: Phantom Deadfall", ColonReplacementFormat.SpaceDashSpace, "Christopher Hopper - Series - Ruins of the Earth - Fake - Phantom Deadfall (2021)")]
|
||||
public void should_replace_colon_followed_by_space_with_expected_result(string bookTitle, ColonReplacementFormat replacementFormat, string expected)
|
||||
{
|
||||
_book.Title = bookTitle;
|
||||
_namingConfig.StandardBookFormat = "{Author Name} - {Book Series - }{Book Title} {(Release Year)}";
|
||||
_namingConfig.ColonReplacementFormat = replacementFormat;
|
||||
|
||||
Subject.BuildBookFileName(_author, _edition, _bookFile)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase("Author:Name", ColonReplacementFormat.Smart, "Author-Name")]
|
||||
[TestCase("Author:Name", ColonReplacementFormat.Dash, "Author-Name")]
|
||||
[TestCase("Author:Name", ColonReplacementFormat.Delete, "AuthorName")]
|
||||
[TestCase("Author:Name", ColonReplacementFormat.SpaceDash, "Author -Name")]
|
||||
[TestCase("Author:Name", ColonReplacementFormat.SpaceDashSpace, "Author - Name")]
|
||||
public void should_replace_colon_with_expected_result(string authorName, ColonReplacementFormat replacementFormat, string expected)
|
||||
{
|
||||
_author.Name = authorName;
|
||||
_namingConfig.StandardBookFormat = "{Author Name}";
|
||||
_namingConfig.ColonReplacementFormat = replacementFormat;
|
||||
|
||||
Subject.BuildBookFileName(_author, _edition, _bookFile)
|
||||
.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(031)]
|
||||
public class add_colon_replacement_to_naming_config : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("NamingConfig").AddColumn("ColonReplacementFormat").AsInt32().WithDefaultValue(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@
|
||||
"Close": "Close",
|
||||
"CollapseMultipleBooks": "Collapse Multiple Books",
|
||||
"CollapseMultipleBooksHelpText": "Collapse multiple books releasing on the same day",
|
||||
"ColonReplacement": "Colon Replacement",
|
||||
"Columns": "Columns",
|
||||
"CompletedDownloadHandling": "Completed Download Handling",
|
||||
"Component": "Component",
|
||||
@@ -158,6 +159,7 @@
|
||||
"CutoffHelpText": "Once this quality is reached Readarr will no longer download books",
|
||||
"CutoffUnmet": "Cutoff Unmet",
|
||||
"DBMigration": "DB Migration",
|
||||
"DashOrSpaceDashDependingOnName": "Dash or Space Dash depending on name",
|
||||
"DataAllBooks": "Monitor all books",
|
||||
"DataExistingBooks": "Monitor books that have files or have not released yet",
|
||||
"DataFirstBook": "Monitor the first book. All other books will be ignored",
|
||||
@@ -662,6 +664,9 @@
|
||||
"Reorder": "Reorder",
|
||||
"ReplaceIllegalCharacters": "Replace Illegal Characters",
|
||||
"ReplaceIllegalCharactersHelpText": "Replace illegal characters. If unchecked, Readarr will remove them instead",
|
||||
"ReplaceWithDash": "Replace with Dash",
|
||||
"ReplaceWithSpaceDash": "Replace with Space Dash",
|
||||
"ReplaceWithSpaceDashSpace": "Replace with Space Dash Space",
|
||||
"RequiredHelpText": "This {0} condition must match for the custom format to apply. Otherwise a single {0} match is sufficient.",
|
||||
"RequiredPlaceHolder": "Add new restriction",
|
||||
"RescanAfterRefreshHelpText": "Rescan the author folder after refreshing the author",
|
||||
@@ -766,6 +771,7 @@
|
||||
"SkipRedownload": "Skip Redownload",
|
||||
"SkipSecondarySeriesBooks": "Skip secondary series books",
|
||||
"SkipredownloadHelpText": "Prevents Readarr from trying download alternative releases for the removed items",
|
||||
"SmartReplace": "Smart Replace",
|
||||
"SorryThatAuthorCannotBeFound": "Sorry, that author cannot be found.",
|
||||
"SorryThatBookCannotBeFound": "Sorry, that book cannot be found.",
|
||||
"Source": "Source",
|
||||
|
||||
@@ -223,24 +223,17 @@ namespace NzbDrone.Core.Organizer
|
||||
return TitlePrefixRegex.Replace(title, "$2, $1$3");
|
||||
}
|
||||
|
||||
public static string CleanFileName(string name, bool replace = true)
|
||||
public static string CleanFileName(string name)
|
||||
{
|
||||
string result = name;
|
||||
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", ":", "|", "\"" };
|
||||
string[] goodCharacters = { "+", "+", "", "", "!", "-", "-", "", "" };
|
||||
|
||||
for (int i = 0; i < badCharacters.Length; i++)
|
||||
{
|
||||
result = result.Replace(badCharacters[i], replace ? goodCharacters[i] : string.Empty);
|
||||
}
|
||||
|
||||
return result.Trim();
|
||||
return CleanFileName(name, NamingConfig.Default);
|
||||
}
|
||||
|
||||
public static string CleanFolderName(string name)
|
||||
{
|
||||
name = FileNameCleanupRegex.Replace(name, match => match.Captures[0].Value[0].ToString());
|
||||
return name.Trim(' ', '.');
|
||||
name = name.Trim(' ', '.');
|
||||
|
||||
return CleanFileName(name);
|
||||
}
|
||||
|
||||
private void AddAuthorTokens(Dictionary<string, Func<TokenMatch, string>> tokenHandlers, Author author)
|
||||
@@ -420,7 +413,7 @@ namespace NzbDrone.Core.Organizer
|
||||
replacementText = replacementText.Replace(" ", tokenMatch.Separator);
|
||||
}
|
||||
|
||||
replacementText = CleanFileName(replacementText, namingConfig.ReplaceIllegalCharacters);
|
||||
replacementText = CleanFileName(replacementText, namingConfig);
|
||||
|
||||
if (!replacementText.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -504,6 +497,53 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
return Path.GetFileNameWithoutExtension(bookFile.Path);
|
||||
}
|
||||
|
||||
private static string CleanFileName(string name, NamingConfig namingConfig)
|
||||
{
|
||||
var result = name;
|
||||
string[] badCharacters = { "\\", "/", "<", ">", "?", "*", "|", "\"" };
|
||||
string[] goodCharacters = { "+", "+", "", "", "!", "-", "", "" };
|
||||
|
||||
if (namingConfig.ReplaceIllegalCharacters)
|
||||
{
|
||||
// Smart replaces a colon followed by a space with space dash space for a better appearance
|
||||
if (namingConfig.ColonReplacementFormat == ColonReplacementFormat.Smart)
|
||||
{
|
||||
result = result.Replace(": ", " - ");
|
||||
result = result.Replace(":", "-");
|
||||
}
|
||||
else
|
||||
{
|
||||
var replacement = string.Empty;
|
||||
|
||||
switch (namingConfig.ColonReplacementFormat)
|
||||
{
|
||||
case ColonReplacementFormat.Dash:
|
||||
replacement = "-";
|
||||
break;
|
||||
case ColonReplacementFormat.SpaceDash:
|
||||
replacement = " -";
|
||||
break;
|
||||
case ColonReplacementFormat.SpaceDashSpace:
|
||||
replacement = " - ";
|
||||
break;
|
||||
}
|
||||
|
||||
result = result.Replace(":", replacement);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.Replace(":", string.Empty);
|
||||
}
|
||||
|
||||
for (var i = 0; i < badCharacters.Length; i++)
|
||||
{
|
||||
result = result.Replace(badCharacters[i], namingConfig.ReplaceIllegalCharacters ? goodCharacters[i] : string.Empty);
|
||||
}
|
||||
|
||||
return result.TrimStart(' ', '.').TrimEnd(' ');
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TokenMatch
|
||||
@@ -527,4 +567,13 @@ namespace NzbDrone.Core.Organizer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ColonReplacementFormat
|
||||
{
|
||||
Delete = 0,
|
||||
Dash = 1,
|
||||
SpaceDash = 2,
|
||||
SpaceDashSpace = 3,
|
||||
Smart = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ namespace NzbDrone.Core.Organizer
|
||||
{
|
||||
RenameBooks = false,
|
||||
ReplaceIllegalCharacters = true,
|
||||
ColonReplacementFormat = ColonReplacementFormat.Smart,
|
||||
StandardBookFormat = "{Book Title}" + Path.DirectorySeparatorChar + "{Author Name} - {Book Title}{ (PartNumber)}",
|
||||
AuthorFolderFormat = "{Author Name}",
|
||||
};
|
||||
|
||||
public bool RenameBooks { get; set; }
|
||||
public bool ReplaceIllegalCharacters { get; set; }
|
||||
public ColonReplacementFormat ColonReplacementFormat { get; set; }
|
||||
public string StandardBookFormat { get; set; }
|
||||
public string AuthorFolderFormat { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Readarr.Api.V1.Config
|
||||
{
|
||||
public bool RenameBooks { get; set; }
|
||||
public bool ReplaceIllegalCharacters { get; set; }
|
||||
public int ColonReplacementFormat { get; set; }
|
||||
public string StandardBookFormat { get; set; }
|
||||
public string AuthorFolderFormat { get; set; }
|
||||
public bool IncludeAuthorName { get; set; }
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Readarr.Api.V1.Config
|
||||
|
||||
RenameBooks = model.RenameBooks,
|
||||
ReplaceIllegalCharacters = model.ReplaceIllegalCharacters,
|
||||
ColonReplacementFormat = (int)model.ColonReplacementFormat,
|
||||
StandardBookFormat = model.StandardBookFormat,
|
||||
AuthorFolderFormat = model.AuthorFolderFormat
|
||||
};
|
||||
@@ -42,6 +43,7 @@ namespace Readarr.Api.V1.Config
|
||||
|
||||
RenameBooks = resource.RenameBooks,
|
||||
ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters,
|
||||
ColonReplacementFormat = (ColonReplacementFormat)resource.ColonReplacementFormat,
|
||||
StandardBookFormat = resource.StandardBookFormat,
|
||||
AuthorFolderFormat = resource.AuthorFolderFormat,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user