New: Configurable Specials folder format
This commit is contained in:
parent
39ea2dd32f
commit
628ab85de4
|
@ -85,6 +85,16 @@ class Naming extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onSpecialsFolderNamingModalOpenClick = () => {
|
||||
this.setState({
|
||||
isNamingModalOpen: true,
|
||||
namingModalOptions: {
|
||||
name: 'specialsFolderFormat',
|
||||
season: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNamingModalClose = () => {
|
||||
this.setState({ isNamingModalOpen: false });
|
||||
}
|
||||
|
@ -130,6 +140,8 @@ class Naming extends Component {
|
|||
const seriesFolderFormatErrors = [];
|
||||
const seasonFolderFormatHelpTexts = [];
|
||||
const seasonFolderFormatErrors = [];
|
||||
const specialsFolderFormatHelpTexts = [];
|
||||
const specialsFolderFormatErrors = [];
|
||||
|
||||
if (examplesPopulated) {
|
||||
if (examples.singleEpisodeExample) {
|
||||
|
@ -173,6 +185,12 @@ class Naming extends Component {
|
|||
} else {
|
||||
seasonFolderFormatErrors.push({ message: 'Invalid Format' });
|
||||
}
|
||||
|
||||
if (examples.specialsFolderExample) {
|
||||
specialsFolderFormatHelpTexts.push(`Example: ${examples.specialsFolderExample}`);
|
||||
} else {
|
||||
specialsFolderFormatErrors.push({ message: 'Invalid Format' });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -297,6 +315,24 @@ class Naming extends Component {
|
|||
/>
|
||||
</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>
|
||||
<FormLabel>Multi-Episode Style</FormLabel>
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ namespace NzbDrone.Api.Config
|
|||
SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat();
|
||||
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
|
||||
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
|
||||
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
|
||||
}
|
||||
|
||||
private void UpdateNamingConfig(NamingConfigResource resource)
|
||||
|
@ -109,6 +110,10 @@ namespace NzbDrone.Api.Config
|
|||
? "Invalid format"
|
||||
: _filenameSampleService.GetSeasonFolderSample(nameSpec);
|
||||
|
||||
sampleResource.SpecialsFolderExample = nameSpec.SpecialsFolderFormat.IsNullOrWhiteSpace()
|
||||
? "Invalid format"
|
||||
: _filenameSampleService.GetSpecialsFolderSample(nameSpec);
|
||||
|
||||
return sampleResource.AsResponse();
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace NzbDrone.Api.Config
|
|||
public string AnimeEpisodeFormat { get; set; }
|
||||
public string SeriesFolderFormat { get; set; }
|
||||
public string SeasonFolderFormat { get; set; }
|
||||
public string SpecialsFolderFormat { get; set; }
|
||||
public bool IncludeSeriesTitle { get; set; }
|
||||
public bool IncludeEpisodeTitle { get; set; }
|
||||
public bool IncludeQuality { get; set; }
|
||||
|
@ -36,7 +37,8 @@ namespace NzbDrone.Api.Config
|
|||
DailyEpisodeFormat = model.DailyEpisodeFormat,
|
||||
AnimeEpisodeFormat = model.AnimeEpisodeFormat,
|
||||
SeriesFolderFormat = model.SeriesFolderFormat,
|
||||
SeasonFolderFormat = model.SeasonFolderFormat
|
||||
SeasonFolderFormat = model.SeasonFolderFormat,
|
||||
SpecialsFolderFormat = model.SpecialsFolderFormat
|
||||
//IncludeSeriesTitle
|
||||
//IncludeEpisodeTitle
|
||||
//IncludeQuality
|
||||
|
@ -69,7 +71,8 @@ namespace NzbDrone.Api.Config
|
|||
DailyEpisodeFormat = resource.DailyEpisodeFormat,
|
||||
AnimeEpisodeFormat = resource.AnimeEpisodeFormat,
|
||||
SeriesFolderFormat = resource.SeriesFolderFormat,
|
||||
SeasonFolderFormat = resource.SeasonFolderFormat
|
||||
SeasonFolderFormat = resource.SeasonFolderFormat,
|
||||
SpecialsFolderFormat = resource.SpecialsFolderFormat
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,5 +9,6 @@
|
|||
public string AnimeMultiEpisodeExample { get; set; }
|
||||
public string SeriesFolderExample { get; set; }
|
||||
public string SeasonFolderExample { get; set; }
|
||||
public string SpecialsFolderExample { get; set; }
|
||||
}
|
||||
}
|
|
@ -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}", @"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 - 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)
|
||||
{
|
||||
var fakeSeries = Builder<Series>.CreateNew()
|
||||
|
@ -39,6 +39,7 @@ namespace NzbDrone.Core.Test.OrganizerTests
|
|||
.Build();
|
||||
|
||||
namingConfig.SeasonFolderFormat = seasonFolderFormat;
|
||||
namingConfig.SpecialsFolderFormat = "MySpecials";
|
||||
|
||||
Subject.BuildFilePath(fakeSeries, seasonNumber, filename, ".mkv").Should().Be(expectedPath.AsOsAgnostic());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -136,6 +136,7 @@
|
|||
<Compile Include="Configuration\InvalidConfigFileException.cs" />
|
||||
<Compile Include="Configuration\RescanAfterRefreshType.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\131_download_propers_config.cs" />
|
||||
<Compile Include="Datastore\Migration\130_episode_last_searched_time.cs" />
|
||||
|
|
|
@ -173,12 +173,6 @@ namespace NzbDrone.Core.Organizer
|
|||
var path = series.Path;
|
||||
|
||||
if (series.SeasonFolder)
|
||||
{
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
path = Path.Combine(path, "Specials");
|
||||
}
|
||||
else
|
||||
{
|
||||
var seasonFolder = GetSeasonFolder(series, seasonNumber);
|
||||
|
||||
|
@ -186,7 +180,6 @@ namespace NzbDrone.Core.Organizer
|
|||
|
||||
path = Path.Combine(path, seasonFolder);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
@ -266,7 +259,9 @@ namespace NzbDrone.Core.Organizer
|
|||
AddIdTokens(tokenHandlers, series);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.Organizer
|
|||
SampleResult GetAnimeMultiEpisodeSample(NamingConfig nameSpec);
|
||||
string GetSeriesFolderSample(NamingConfig nameSpec);
|
||||
string GetSeasonFolderSample(NamingConfig nameSpec);
|
||||
string GetSpecialsFolderSample(NamingConfig nameSpec);
|
||||
}
|
||||
|
||||
public class FileNameSampleService : IFilenameSampleService
|
||||
|
@ -245,6 +246,11 @@ namespace NzbDrone.Core.Organizer
|
|||
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)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -41,6 +41,11 @@ namespace NzbDrone.Core.Organizer
|
|||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
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
|
||||
|
|
|
@ -13,7 +13,8 @@ namespace NzbDrone.Core.Organizer
|
|||
DailyEpisodeFormat = "{Series Title} - {Air-Date} - {Episode Title} {Quality Full}",
|
||||
AnimeEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}",
|
||||
SeriesFolderFormat = "{Series Title}",
|
||||
SeasonFolderFormat = "Season {season}"
|
||||
SeasonFolderFormat = "Season {season}",
|
||||
SpecialsFolderFormat = "Specials"
|
||||
};
|
||||
|
||||
public bool RenameEpisodes { get; set; }
|
||||
|
@ -24,5 +25,6 @@ namespace NzbDrone.Core.Organizer
|
|||
public string AnimeEpisodeFormat { get; set; }
|
||||
public string SeriesFolderFormat { get; set; }
|
||||
public string SeasonFolderFormat { get; set; }
|
||||
public string SpecialsFolderFormat { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ namespace Sonarr.Api.V3.Config
|
|||
SharedValidator.RuleFor(c => c.AnimeEpisodeFormat).ValidAnimeEpisodeFormat();
|
||||
SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat();
|
||||
SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat();
|
||||
SharedValidator.RuleFor(c => c.SpecialsFolderFormat).ValidSpecialsFolderFormat();
|
||||
}
|
||||
|
||||
private void UpdateNamingConfig(NamingConfigResource resource)
|
||||
|
@ -114,6 +115,10 @@ namespace Sonarr.Api.V3.Config
|
|||
? null
|
||||
: _filenameSampleService.GetSeasonFolderSample(nameSpec);
|
||||
|
||||
sampleResource.SpecialsFolderExample = nameSpec.SpecialsFolderFormat.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _filenameSampleService.GetSpecialsFolderSample(nameSpec);
|
||||
|
||||
return sampleResource.AsResponse();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace Sonarr.Api.V3.Config
|
|||
public string AnimeEpisodeFormat { get; set; }
|
||||
public string SeriesFolderFormat { get; set; }
|
||||
public string SeasonFolderFormat { get; set; }
|
||||
public string SpecialsFolderFormat { get; set; }
|
||||
public bool IncludeSeriesTitle { get; set; }
|
||||
public bool IncludeEpisodeTitle { get; set; }
|
||||
public bool IncludeQuality { get; set; }
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace Sonarr.Api.V3.Config
|
|||
public string AnimeMultiEpisodeExample { get; set; }
|
||||
public string SeriesFolderExample { get; set; }
|
||||
public string SeasonFolderExample { get; set; }
|
||||
public string SpecialsFolderExample { get; set; }
|
||||
}
|
||||
|
||||
public static class NamingConfigResourceMapper
|
||||
|
@ -28,7 +29,8 @@ namespace Sonarr.Api.V3.Config
|
|||
DailyEpisodeFormat = model.DailyEpisodeFormat,
|
||||
AnimeEpisodeFormat = model.AnimeEpisodeFormat,
|
||||
SeriesFolderFormat = model.SeriesFolderFormat,
|
||||
SeasonFolderFormat = model.SeasonFolderFormat
|
||||
SeasonFolderFormat = model.SeasonFolderFormat,
|
||||
SpecialsFolderFormat = model.SpecialsFolderFormat
|
||||
//IncludeSeriesTitle
|
||||
//IncludeEpisodeTitle
|
||||
//IncludeQuality
|
||||
|
@ -61,7 +63,8 @@ namespace Sonarr.Api.V3.Config
|
|||
DailyEpisodeFormat = resource.DailyEpisodeFormat,
|
||||
AnimeEpisodeFormat = resource.AnimeEpisodeFormat,
|
||||
SeriesFolderFormat = resource.SeriesFolderFormat,
|
||||
SeasonFolderFormat = resource.SeasonFolderFormat
|
||||
SeasonFolderFormat = resource.SeasonFolderFormat,
|
||||
SpecialsFolderFormat = resource.SpecialsFolderFormat
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue