New: Configurable Specials folder format

This commit is contained in:
Taloth Saldono 2019-06-10 00:42:37 +02:00
parent 39ea2dd32f
commit 628ab85de4
14 changed files with 111 additions and 17 deletions

View File

@ -85,6 +85,16 @@ class Naming extends Component {
}); });
} }
onSpecialsFolderNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'specialsFolderFormat',
season: true
}
});
}
onNamingModalClose = () => { onNamingModalClose = () => {
this.setState({ isNamingModalOpen: false }); this.setState({ isNamingModalOpen: false });
} }
@ -130,6 +140,8 @@ class Naming extends Component {
const seriesFolderFormatErrors = []; const seriesFolderFormatErrors = [];
const seasonFolderFormatHelpTexts = []; const seasonFolderFormatHelpTexts = [];
const seasonFolderFormatErrors = []; const seasonFolderFormatErrors = [];
const specialsFolderFormatHelpTexts = [];
const specialsFolderFormatErrors = [];
if (examplesPopulated) { if (examplesPopulated) {
if (examples.singleEpisodeExample) { if (examples.singleEpisodeExample) {
@ -173,6 +185,12 @@ class Naming extends Component {
} else { } else {
seasonFolderFormatErrors.push({ message: 'Invalid Format' }); seasonFolderFormatErrors.push({ message: 'Invalid Format' });
} }
if (examples.specialsFolderExample) {
specialsFolderFormatHelpTexts.push(`Example: ${examples.specialsFolderExample}`);
} else {
specialsFolderFormatErrors.push({ message: 'Invalid Format' });
}
} }
return ( return (
@ -297,6 +315,24 @@ class Naming extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>Specials Folder Format</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="specialsFolderFormat"
buttons={<FormInputButton onPress={this.onSpecialsFolderNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.specialsFolderFormat}
helpTexts={specialsFolderFormatHelpTexts}
errors={[...specialsFolderFormatErrors, ...settings.specialsFolderFormat.errors]}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>Multi-Episode Style</FormLabel> <FormLabel>Multi-Episode Style</FormLabel>

View File

@ -41,6 +41,7 @@ namespace NzbDrone.Api.Config
SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat();
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
} }
private void UpdateNamingConfig(NamingConfigResource resource) private void UpdateNamingConfig(NamingConfigResource resource)
@ -109,6 +110,10 @@ namespace NzbDrone.Api.Config
? "Invalid format" ? "Invalid format"
: _filenameSampleService.GetSeasonFolderSample(nameSpec); : _filenameSampleService.GetSeasonFolderSample(nameSpec);
sampleResource.SpecialsFolderExample = nameSpec.SpecialsFolderFormat.IsNullOrWhiteSpace()
? "Invalid format"
: _filenameSampleService.GetSpecialsFolderSample(nameSpec);
return sampleResource.AsResponse(); return sampleResource.AsResponse();
} }

View File

@ -13,6 +13,7 @@ namespace NzbDrone.Api.Config
public string AnimeEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; }
public string SeriesFolderFormat { get; set; } public string SeriesFolderFormat { get; set; }
public string SeasonFolderFormat { get; set; } public string SeasonFolderFormat { get; set; }
public string SpecialsFolderFormat { get; set; }
public bool IncludeSeriesTitle { get; set; } public bool IncludeSeriesTitle { get; set; }
public bool IncludeEpisodeTitle { get; set; } public bool IncludeEpisodeTitle { get; set; }
public bool IncludeQuality { get; set; } public bool IncludeQuality { get; set; }
@ -36,7 +37,8 @@ namespace NzbDrone.Api.Config
DailyEpisodeFormat = model.DailyEpisodeFormat, DailyEpisodeFormat = model.DailyEpisodeFormat,
AnimeEpisodeFormat = model.AnimeEpisodeFormat, AnimeEpisodeFormat = model.AnimeEpisodeFormat,
SeriesFolderFormat = model.SeriesFolderFormat, SeriesFolderFormat = model.SeriesFolderFormat,
SeasonFolderFormat = model.SeasonFolderFormat SeasonFolderFormat = model.SeasonFolderFormat,
SpecialsFolderFormat = model.SpecialsFolderFormat
//IncludeSeriesTitle //IncludeSeriesTitle
//IncludeEpisodeTitle //IncludeEpisodeTitle
//IncludeQuality //IncludeQuality
@ -69,7 +71,8 @@ namespace NzbDrone.Api.Config
DailyEpisodeFormat = resource.DailyEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat,
SeriesFolderFormat = resource.SeriesFolderFormat, SeriesFolderFormat = resource.SeriesFolderFormat,
SeasonFolderFormat = resource.SeasonFolderFormat SeasonFolderFormat = resource.SeasonFolderFormat,
SpecialsFolderFormat = resource.SpecialsFolderFormat
}; };
} }
} }

View File

@ -9,5 +9,6 @@
public string AnimeMultiEpisodeExample { get; set; } public string AnimeMultiEpisodeExample { get; set; }
public string SeriesFolderExample { get; set; } public string SeriesFolderExample { get; set; }
public string SeasonFolderExample { get; set; } public string SeasonFolderExample { get; set; }
public string SpecialsFolderExample { get; set; }
} }
} }

View File

@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.OrganizerTests
[TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season:00}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season:00}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")]
[TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, false, "Season {season}", @"C:\Test\30 Rock\30 Rock - S01E05 - Episode Title.mkv")]
[TestCase("30 Rock - S01E05 - Episode Title", 1, true, "ReallyUglySeasonFolder {season}", @"C:\Test\30 Rock\ReallyUglySeasonFolder 1\30 Rock - S01E05 - Episode Title.mkv")] [TestCase("30 Rock - S01E05 - Episode Title", 1, true, "ReallyUglySeasonFolder {season}", @"C:\Test\30 Rock\ReallyUglySeasonFolder 1\30 Rock - S01E05 - Episode Title.mkv")]
[TestCase("30 Rock - S00E05 - Episode Title", 0, true, "Season {season}", @"C:\Test\30 Rock\Specials\30 Rock - S00E05 - Episode Title.mkv")] [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) public void CalculateFilePath_SeasonFolder_SingleNumber(string filename, int seasonNumber, bool useSeasonFolder, string seasonFolderFormat, string expectedPath)
{ {
var fakeSeries = Builder<Series>.CreateNew() var fakeSeries = Builder<Series>.CreateNew()
@ -39,6 +39,7 @@ namespace NzbDrone.Core.Test.OrganizerTests
.Build(); .Build();
namingConfig.SeasonFolderFormat = seasonFolderFormat; namingConfig.SeasonFolderFormat = seasonFolderFormat;
namingConfig.SpecialsFolderFormat = "MySpecials";
Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic()); Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
} }

View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(134)]
public class add_specials_folder_format : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("NamingConfig").AddColumn("SpecialsFolderFormat").AsString().Nullable();
Execute.WithConnection(ConvertConfig);
}
private void ConvertConfig(IDbConnection conn, IDbTransaction tran)
{
var defaultFormat = "Specials";
using (IDbCommand updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE NamingConfig SET SpecialsFolderFormat = ?";
updateCmd.AddParameter(defaultFormat);
updateCmd.ExecuteNonQuery();
}
}
}
}

View File

@ -136,6 +136,7 @@
<Compile Include="Configuration\InvalidConfigFileException.cs" /> <Compile Include="Configuration\InvalidConfigFileException.cs" />
<Compile Include="Configuration\RescanAfterRefreshType.cs" /> <Compile Include="Configuration\RescanAfterRefreshType.cs" />
<Compile Include="Configuration\ResetApiKeyCommand.cs" /> <Compile Include="Configuration\ResetApiKeyCommand.cs" />
<Compile Include="Datastore\Migration\134_add_specials_folder_format.cs" />
<Compile Include="Datastore\Migration\132_add_download_client_priority.cs" /> <Compile Include="Datastore\Migration\132_add_download_client_priority.cs" />
<Compile Include="Datastore\Migration\131_download_propers_config.cs" /> <Compile Include="Datastore\Migration\131_download_propers_config.cs" />
<Compile Include="Datastore\Migration\130_episode_last_searched_time.cs" /> <Compile Include="Datastore\Migration\130_episode_last_searched_time.cs" />

View File

@ -173,12 +173,6 @@ namespace NzbDrone.Core.Organizer
var path = series.Path; var path = series.Path;
if (series.SeasonFolder) if (series.SeasonFolder)
{
if (seasonNumber == 0)
{
path = Path.Combine(path, "Specials");
}
else
{ {
var seasonFolder = GetSeasonFolder(series, seasonNumber); var seasonFolder = GetSeasonFolder(series, seasonNumber);
@ -186,7 +180,6 @@ namespace NzbDrone.Core.Organizer
path = Path.Combine(path, seasonFolder); path = Path.Combine(path, seasonFolder);
} }
}
return path; return path;
} }
@ -266,7 +259,9 @@ namespace NzbDrone.Core.Organizer
AddIdTokens(tokenHandlers, series); AddIdTokens(tokenHandlers, series);
AddSeasonTokens(tokenHandlers, seasonNumber); AddSeasonTokens(tokenHandlers, seasonNumber);
var folderName = ReplaceTokens(namingConfig.SeasonFolderFormat, tokenHandlers, namingConfig); var format = seasonNumber == 0 ? namingConfig.SpecialsFolderFormat : namingConfig.SeasonFolderFormat;
var folderName = ReplaceTokens(format, tokenHandlers, namingConfig);
return CleanFolderName(folderName); return CleanFolderName(folderName);
} }

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Core.Organizer
SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec); SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec);
string GetSeriesFolderSample(NamingConfig nameSpec); string GetSeriesFolderSample(NamingConfig nameSpec);
string GetSeasonFolderSample(NamingConfig nameSpec); string GetSeasonFolderSample(NamingConfig nameSpec);
string GetSpecialsFolderSample(NamingConfig nameSpec);
} }
public class FileNameSampleService : IFilenameSampleService public class FileNameSampleService : IFilenameSampleService
@ -245,6 +246,11 @@ namespace NzbDrone.Core.Organizer
return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec); return _buildFileNames.GetSeasonFolder(_standardSeries, _episode1.SeasonNumber, nameSpec);
} }
public string GetSpecialsFolderSample(NamingConfig nameSpec)
{
return _buildFileNames.GetSeasonFolder(_standardSeries, 0, nameSpec);
}
private string BuildSample(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) private string BuildSample(List<Episode> episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec)
{ {
try try

View File

@ -41,6 +41,11 @@ namespace NzbDrone.Core.Organizer
ruleBuilder.SetValidator(new NotEmptyValidator(null)); ruleBuilder.SetValidator(new NotEmptyValidator(null));
return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number"); return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number");
} }
public static IRuleBuilderOptions<T, string> ValidSpecialsFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.SetValidator(new NotEmptyValidator(null));
}
} }
public class ValidStandardEpisodeFormatValidator : PropertyValidator public class ValidStandardEpisodeFormatValidator : PropertyValidator

View File

@ -13,7 +13,8 @@ namespace NzbDrone.Core.Organizer
DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}", DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}",
AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}", AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}",
SeriesFolderFormat = "{Series Title}", SeriesFolderFormat = "{Series Title}",
SeasonFolderFormat = "Season {season}" SeasonFolderFormat = "Season {season}",
SpecialsFolderFormat = "Specials"
}; };
public bool RenameEpisodes { get; set; } public bool RenameEpisodes { get; set; }
@ -24,5 +25,6 @@ namespace NzbDrone.Core.Organizer
public string AnimeEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; }
public string SeriesFolderFormat { get; set; } public string SeriesFolderFormat { get; set; }
public string SeasonFolderFormat { get; set; } public string SeasonFolderFormat { get; set; }
public string SpecialsFolderFormat { get; set; }
} }
} }

View File

@ -41,6 +41,7 @@ namespace Sonarr.Api.V3.Config
SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat(); SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat();
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
} }
private void UpdateNamingConfig(NamingConfigResource resource) private void UpdateNamingConfig(NamingConfigResource resource)
@ -114,6 +115,10 @@ namespace Sonarr.Api.V3.Config
? null ? null
: _filenameSampleService.GetSeasonFolderSample(nameSpec); : _filenameSampleService.GetSeasonFolderSample(nameSpec);
sampleResource.SpecialsFolderExample = nameSpec.SpecialsFolderFormat.IsNullOrWhiteSpace()
? null
: _filenameSampleService.GetSpecialsFolderSample(nameSpec);
return sampleResource.AsResponse(); return sampleResource.AsResponse();
} }

View File

@ -12,6 +12,7 @@ namespace Sonarr.Api.V3.Config
public string AnimeEpisodeFormat { get; set; } public string AnimeEpisodeFormat { get; set; }
public string SeriesFolderFormat { get; set; } public string SeriesFolderFormat { get; set; }
public string SeasonFolderFormat { get; set; } public string SeasonFolderFormat { get; set; }
public string SpecialsFolderFormat { get; set; }
public bool IncludeSeriesTitle { get; set; } public bool IncludeSeriesTitle { get; set; }
public bool IncludeEpisodeTitle { get; set; } public bool IncludeEpisodeTitle { get; set; }
public bool IncludeQuality { get; set; } public bool IncludeQuality { get; set; }

View File

@ -11,6 +11,7 @@ namespace Sonarr.Api.V3.Config
public string AnimeMultiEpisodeExample { get; set; } public string AnimeMultiEpisodeExample { get; set; }
public string SeriesFolderExample { get; set; } public string SeriesFolderExample { get; set; }
public string SeasonFolderExample { get; set; } public string SeasonFolderExample { get; set; }
public string SpecialsFolderExample { get; set; }
} }
public static class NamingConfigResourceMapper public static class NamingConfigResourceMapper
@ -28,7 +29,8 @@ namespace Sonarr.Api.V3.Config
DailyEpisodeFormat = model.DailyEpisodeFormat, DailyEpisodeFormat = model.DailyEpisodeFormat,
AnimeEpisodeFormat = model.AnimeEpisodeFormat, AnimeEpisodeFormat = model.AnimeEpisodeFormat,
SeriesFolderFormat = model.SeriesFolderFormat, SeriesFolderFormat = model.SeriesFolderFormat,
SeasonFolderFormat = model.SeasonFolderFormat SeasonFolderFormat = model.SeasonFolderFormat,
SpecialsFolderFormat = model.SpecialsFolderFormat
//IncludeSeriesTitle //IncludeSeriesTitle
//IncludeEpisodeTitle //IncludeEpisodeTitle
//IncludeQuality //IncludeQuality
@ -61,7 +63,8 @@ namespace Sonarr.Api.V3.Config
DailyEpisodeFormat = resource.DailyEpisodeFormat, DailyEpisodeFormat = resource.DailyEpisodeFormat,
AnimeEpisodeFormat = resource.AnimeEpisodeFormat, AnimeEpisodeFormat = resource.AnimeEpisodeFormat,
SeriesFolderFormat = resource.SeriesFolderFormat, SeriesFolderFormat = resource.SeriesFolderFormat,
SeasonFolderFormat = resource.SeasonFolderFormat SeasonFolderFormat = resource.SeasonFolderFormat,
SpecialsFolderFormat = resource.SpecialsFolderFormat
}; };
} }
} }