diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index ef22d320a..81367c831 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -40,6 +40,18 @@ class Naming extends Component { }); } + onMultiDiscNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'multiDiscTrackFormat', + album: true, + track: true, + additional: true + } + }); + } + onArtistFolderNamingModalOpenClick = () => { this.setState({ isNamingModalOpen: true, @@ -87,6 +99,8 @@ class Naming extends Component { const standardTrackFormatHelpTexts = []; const standardTrackFormatErrors = []; + const multiDiscTrackFormatHelpTexts = []; + const multiDiscTrackFormatErrors = []; const artistFolderFormatHelpTexts = []; const artistFolderFormatErrors = []; const albumFolderFormatHelpTexts = []; @@ -99,6 +113,12 @@ class Naming extends Component { standardTrackFormatErrors.push({ message: 'Single Track: Invalid Format' }); } + if (examples.multiDiscTrackExample) { + multiDiscTrackFormatHelpTexts.push(`Multi Disc Track: ${examples.multiDiscTrackExample}`); + } else { + multiDiscTrackFormatErrors.push({ message: 'Single Track: Invalid Format' }); + } + if (examples.artistFolderExample) { artistFolderFormatHelpTexts.push(`Example: ${examples.artistFolderExample}`); } else { @@ -169,6 +189,21 @@ class Naming extends Component { /> + + Multi Disc Track Format + + ?} + onChange={onInputChange} + {...settings.multiDiscTrackFormat} + helpTexts={multiDiscTrackFormatHelpTexts} + errors={[...multiDiscTrackFormatErrors, ...settings.multiDiscTrackFormat.errors]} + /> + + } diff --git a/src/Lidarr.Api.V1/Config/NamingConfigModule.cs b/src/Lidarr.Api.V1/Config/NamingConfigModule.cs index 4af476d20..c2ba93c5c 100644 --- a/src/Lidarr.Api.V1/Config/NamingConfigModule.cs +++ b/src/Lidarr.Api.V1/Config/NamingConfigModule.cs @@ -37,6 +37,7 @@ namespace Lidarr.Api.V1.Config SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidTrackFormat(); + SharedValidator.RuleFor(c => c.MultiDiscTrackFormat).ValidTrackFormat(); SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidArtistFolderFormat(); SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidAlbumFolderFormat(); } @@ -60,6 +61,12 @@ namespace Lidarr.Api.V1.Config basicConfig.AddToResource(resource); } + if (resource.MultiDiscTrackFormat.IsNotNullOrWhiteSpace()) + { + var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); + basicConfig.AddToResource(resource); + } + return resource; } @@ -79,11 +86,16 @@ namespace Lidarr.Api.V1.Config var sampleResource = new NamingExampleResource(); var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); + var multiDiscTrackSampleResult = _filenameSampleService.GetMultiDiscTrackSample(nameSpec); sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null ? null : singleTrackSampleResult.FileName; + sampleResource.MultiDiscTrackExample = _filenameValidationService.ValidateTrackFilename(multiDiscTrackSampleResult) != null + ? null + : multiDiscTrackSampleResult.FileName; + sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() ? null : _filenameSampleService.GetArtistFolderSample(nameSpec); diff --git a/src/Lidarr.Api.V1/Config/NamingConfigResource.cs b/src/Lidarr.Api.V1/Config/NamingConfigResource.cs index fce16aa99..ee39b9040 100644 --- a/src/Lidarr.Api.V1/Config/NamingConfigResource.cs +++ b/src/Lidarr.Api.V1/Config/NamingConfigResource.cs @@ -7,6 +7,7 @@ namespace Lidarr.Api.V1.Config public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public string StandardTrackFormat { get; set; } + public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; } public string AlbumFolderFormat { get; set; } public bool IncludeArtistName { get; set; } diff --git a/src/Lidarr.Api.V1/Config/NamingExampleResource.cs b/src/Lidarr.Api.V1/Config/NamingExampleResource.cs index 19c281e8a..7a12db6ea 100644 --- a/src/Lidarr.Api.V1/Config/NamingExampleResource.cs +++ b/src/Lidarr.Api.V1/Config/NamingExampleResource.cs @@ -5,6 +5,7 @@ namespace Lidarr.Api.V1.Config public class NamingExampleResource { public string SingleTrackExample { get; set; } + public string MultiDiscTrackExample { get; set; } public string ArtistFolderExample { get; set; } public string AlbumFolderExample { get; set; } } @@ -20,6 +21,7 @@ namespace Lidarr.Api.V1.Config RenameTracks = model.RenameTracks, ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, StandardTrackFormat = model.StandardTrackFormat, + MultiDiscTrackFormat = model.MultiDiscTrackFormat, ArtistFolderFormat = model.ArtistFolderFormat, AlbumFolderFormat = model.AlbumFolderFormat }; @@ -44,6 +46,7 @@ namespace Lidarr.Api.V1.Config RenameTracks = resource.RenameTracks, ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, StandardTrackFormat = resource.StandardTrackFormat, + MultiDiscTrackFormat = resource.MultiDiscTrackFormat, ArtistFolderFormat = resource.ArtistFolderFormat, AlbumFolderFormat = resource.AlbumFolderFormat diff --git a/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs b/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs new file mode 100644 index 000000000..e1f9b915b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/035_multi_disc_naming_format.cs @@ -0,0 +1,17 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System.Data; +using System.IO; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(35)] + public class multi_disc_naming_format : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("NamingConfig").AddColumn("MultiDiscTrackFormat").AsString().Nullable(); + Execute.Sql("UPDATE NamingConfig SET MultiDiscTrackFormat = '{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}'"); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs b/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs deleted file mode 100644 index d68549f07..000000000 --- a/src/NzbDrone.Core/Organizer/EpisodeSortingType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace NzbDrone.Core.Organizer -{ - public class EpisodeSortingType - { - public int Id { get; set; } - public string Name { get; set; } - public string Pattern { get; set; } - public string EpisodeSeparator { get; set; } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index 15d1966ed..4bdb6b2d6 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -66,7 +66,7 @@ namespace NzbDrone.Core.Organizer //TODO: Support Written numbers (One, Two, etc) and Roman Numerals (I, II, III etc) private static readonly Regex MultiPartCleanupRegex = new Regex(@"(?:\(\d+\)|(Part|Pt\.?)\s?\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly char[] EpisodeTitleTrimCharacters = new[] { ' ', '.', '?' }; + private static readonly char[] TrackTitleTrimCharacters = new[] { ' ', '.', '?' }; private static readonly Regex TitlePrefixRegex = new Regex(@"^(The|An|A) (.*?)((?: *\([^)]+\))*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -96,18 +96,27 @@ namespace NzbDrone.Core.Organizer return GetOriginalFileName(trackFile); } - if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace()) + if (namingConfig.StandardTrackFormat.IsNullOrWhiteSpace() || namingConfig.MultiDiscTrackFormat.IsNullOrWhiteSpace()) { - throw new NamingFormatException("Standard track format cannot be empty"); + throw new NamingFormatException("Standard and Multi track formats cannot be empty"); } var pattern = namingConfig.StandardTrackFormat; + + if (tracks.First().AlbumRelease.Value.Media.Count() > 1) + { + pattern = namingConfig.MultiDiscTrackFormat; + } + + var subFolders = pattern.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + var safePattern = subFolders.Aggregate("", (current, folderLevel) => Path.Combine(current, (folderLevel))); + var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); - pattern = FormatTrackNumberTokens(pattern, "", tracks); - pattern = FormatMediumNumberTokens(pattern, "", tracks); + safePattern = FormatTrackNumberTokens(safePattern, "", tracks); + safePattern = FormatMediumNumberTokens(safePattern, "", tracks); AddArtistTokens(tokenHandlers, artist); AddAlbumTokens(tokenHandlers, album); @@ -118,7 +127,7 @@ namespace NzbDrone.Core.Organizer AddMediaInfoTokens(tokenHandlers, trackFile); AddPreferredWords(tokenHandlers, artist, trackFile, preferredWords); - var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); + var fileName = ReplaceTokens(safePattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); fileName = TrimSeparatorsRegex.Replace(fileName, string.Empty); @@ -315,12 +324,12 @@ namespace NzbDrone.Core.Organizer private void AddQualityTokens(Dictionary> tokenHandlers, Artist artist, TrackFile trackFile) { var qualityTitle = _qualityDefinitionService.Get(trackFile.Quality.Quality).Title; - //var qualityProper = GetQualityProper(artist, trackFile.Quality); + var qualityProper = GetQualityProper(trackFile.Quality); //var qualityReal = GetQualityReal(artist, trackFile.Quality); tokenHandlers["{Quality Full}"] = m => String.Format("{0}", qualityTitle); tokenHandlers["{Quality Title}"] = m => qualityTitle; - //tokenHandlers["{Quality Proper}"] = m => qualityProper; + tokenHandlers["{Quality Proper}"] = m => qualityProper; //tokenHandlers["{Quality Real}"] = m => qualityReal; } @@ -459,17 +468,17 @@ namespace NzbDrone.Core.Organizer if (tracks.Count == 1) { - return tracks.First().Title.TrimEnd(EpisodeTitleTrimCharacters); + return tracks.First().Title.TrimEnd(TrackTitleTrimCharacters); } - var titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + var titles = tracks.Select(c => c.Title.TrimEnd(TrackTitleTrimCharacters)) .Select(CleanupTrackTitle) .Distinct() .ToList(); if (titles.All(t => t.IsNullOrWhiteSpace())) { - titles = tracks.Select(c => c.Title.TrimEnd(EpisodeTitleTrimCharacters)) + titles = tracks.Select(c => c.Title.TrimEnd(TrackTitleTrimCharacters)) .Distinct() .ToList(); } @@ -483,21 +492,20 @@ namespace NzbDrone.Core.Organizer return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); } - // TODO: DO WE NEED FOR MUSIC? - //private string GetQualityProper(Series series, QualityModel quality) - //{ - // if (quality.Revision.Version > 1) - // { - // if (series.SeriesType == SeriesTypes.Anime) - // { - // return "v" + quality.Revision.Version; - // } + private string GetQualityProper(QualityModel quality) + { + if (quality.Revision.Version > 1) + { + if (quality.Revision.IsRepack) + { + return "Repack"; + } - // return "Proper"; - // } + return "Proper"; + } - // return String.Empty; - //} + return String.Empty; + } //private string GetQualityReal(Series series, QualityModel quality) //{ diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 35a9c42a2..5ded480da 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -9,7 +9,7 @@ namespace NzbDrone.Core.Organizer public interface IFilenameSampleService { SampleResult GetStandardTrackSample(NamingConfig nameSpec); - + SampleResult GetMultiDiscTrackSample(NamingConfig nameSpec); string GetArtistFolderSample(NamingConfig nameSpec); string GetAlbumFolderSample(NamingConfig nameSpec); } @@ -20,6 +20,8 @@ namespace NzbDrone.Core.Organizer private static Artist _standardArtist; private static Album _standardAlbum; + private static AlbumRelease _singleRelease; + private static AlbumRelease _multiRelease; private static Track _track1; private static List _singleTrack; private static TrackFile _singleTrackFile; @@ -47,7 +49,7 @@ namespace NzbDrone.Core.Organizer Disambiguation = "The Best Album", }; - var _release = new AlbumRelease + _singleRelease = new AlbumRelease { Album = _standardAlbum, Media = new List @@ -62,9 +64,30 @@ namespace NzbDrone.Core.Organizer Monitored = true }; + _multiRelease = new AlbumRelease + { + Album = _standardAlbum, + Media = new List + { + new Medium + { + Name = "CD 1: First Years", + Format = "CD", + Number = 1 + }, + new Medium + { + Name = "CD 2: Second Best", + Format = "CD", + Number = 2 + } + }, + Monitored = true + }; + _track1 = new Track { - AlbumRelease = _release, + AlbumRelease = _singleRelease, AbsoluteTrackNumber = 3, MediumNumber = 1, @@ -102,6 +125,24 @@ namespace NzbDrone.Core.Organizer public SampleResult GetStandardTrackSample(NamingConfig nameSpec) { + _track1.AlbumRelease = _singleRelease; + + var result = new SampleResult + { + FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), + Artist = _standardArtist, + Album = _standardAlbum, + Tracks = _singleTrack, + TrackFile = _singleTrackFile + }; + + return result; + } + + public SampleResult GetMultiDiscTrackSample(NamingConfig nameSpec) + { + _track1.AlbumRelease = _multiRelease; + var result = new SampleResult { FileName = BuildTrackSample(_singleTrack, _standardArtist, _standardAlbum, _singleTrackFile, nameSpec), diff --git a/src/NzbDrone.Core/Organizer/NamingConfig.cs b/src/NzbDrone.Core/Organizer/NamingConfig.cs index 18fda7b8d..4226200d5 100644 --- a/src/NzbDrone.Core/Organizer/NamingConfig.cs +++ b/src/NzbDrone.Core/Organizer/NamingConfig.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Organizer RenameTracks = false, ReplaceIllegalCharacters = true, StandardTrackFormat = "{Artist Name} - {Album Title} - {track:00} - {Track Title}", + MultiDiscTrackFormat = "{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}", ArtistFolderFormat = "{Artist Name}", AlbumFolderFormat = "{Album Title} ({Release Year})" }; @@ -16,6 +17,7 @@ namespace NzbDrone.Core.Organizer public bool RenameTracks { get; set; } public bool ReplaceIllegalCharacters { get; set; } public string StandardTrackFormat { get; set; } + public string MultiDiscTrackFormat { get; set; } public string ArtistFolderFormat { get; set; } public string AlbumFolderFormat { get; set; } }