diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index f8d797d53..470553638 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -65,7 +65,6 @@ class InteractiveImportRow extends Component { album, tracks, isSingleFileRelease, - cuesheetPath, quality, isSelected, onValidRowChange @@ -84,7 +83,7 @@ class InteractiveImportRow extends Component { const isValid = !!( artist && album && - ((isSingleFileRelease && cuesheetPath) || tracks.length) && + (isSingleFileRelease || tracks.length) && quality ); @@ -261,7 +260,7 @@ class InteractiveImportRow extends Component { @@ -269,7 +268,7 @@ class InteractiveImportRow extends Component { showTrackNumbersLoading && } { - !isSingleFileRelease && showTrackNumbersPlaceholder ? : trackNumbers + showTrackNumbersPlaceholder ? : trackNumbers } diff --git a/src/NzbDrone.Core/MediaFiles/CueSheet.cs b/src/NzbDrone.Core/MediaFiles/CueSheet.cs new file mode 100644 index 000000000..bcf330f58 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/CueSheet.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MediaFiles +{ + public class CueSheet : ModelBase + { + public CueSheet(IFileInfo fileInfo) + { + using (var fs = fileInfo.OpenRead()) + { + var bytes = new byte[fileInfo.Length]; + var encoding = new UTF8Encoding(true); + string content; + while (fs.Read(bytes, 0, bytes.Length) > 0) + { + content = encoding.GetString(bytes); + var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + + // Single-file cue means it's an unsplit image + var fileNames = ReadFieldFromCuesheet(lines, "FILE"); + IsSingleFileRelease = fileNames.Count == 1; + FileName = fileNames[0]; + + var performers = ReadFieldFromCuesheet(lines, "PERFORMER"); + if (performers.Count > 0) + { + Performer = performers[0]; + } + + var titles = ReadFieldFromCuesheet(lines, "TITLE"); + if (titles.Count > 0) + { + Title = titles[0]; + } + + Date = ReadOptionalFieldFromCuesheet(lines, "REM DATE"); + } + } + } + + public bool IsSingleFileRelease { get; set; } + public string FileName { get; set; } + public string Title { get; set; } + public string Performer { get; set; } + public string Date { get; set; } + + private static List ReadFieldFromCuesheet(string[] lines, string fieldName) + { + var results = new List(); + var candidates = lines.Where(l => l.StartsWith(fieldName)).ToList(); + foreach (var candidate in candidates) + { + var matches = Regex.Matches(candidate, "\"(.*?)\""); + var result = matches.ToList()[0].Groups[1].Value; + results.Add(result); + } + + return results; + } + + private static string ReadOptionalFieldFromCuesheet(string[] lines, string fieldName) + { + var results = lines.Where(l => l.StartsWith(fieldName)); + if (results.Any()) + { + var matches = Regex.Matches(results.ToList()[0], fieldName + " (.+)"); + var result = matches.ToList()[0].Groups[1].Value; + return result; + } + + return ""; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs index 9afb5e927..63d57512f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs @@ -65,13 +65,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators || tracks.Any(x => x.FileTrackInfo.DiscNumber == 0)) { _logger.Debug("Missing data in tags, trying filename augmentation"); - if (tracks.Count == 1 && tracks[0].IsSingleFileRelease) + if (release.IsSingleFileRelease) { - tracks[0].FileTrackInfo.ArtistTitle = tracks[0].Artist.Name; - tracks[0].FileTrackInfo.AlbumTitle = tracks[0].Album.Title; + for (var i = 0; i < tracks.Count; ++i) + { + tracks[i].FileTrackInfo.ArtistTitle = tracks[i].Artist.Name; + tracks[i].FileTrackInfo.AlbumTitle = tracks[i].Album.Title; + tracks[i].FileTrackInfo.DiscNumber = i + 1; + tracks[i].FileTrackInfo.DiscCount = tracks.Count; - // TODO this is too bold, the release year is not the one from the .cue file - tracks[0].FileTrackInfo.Year = (uint)tracks[0].Album.ReleaseDate.Value.Year; + // TODO this is too bold, the release year is not the one from the .cue file + tracks[i].FileTrackInfo.Year = (uint)tracks[i].Album.ReleaseDate.Value.Year; + } } else { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs index 1d3e0e140..f74508c3a 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs @@ -131,11 +131,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification private List GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting) { - if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + if (localAlbumRelease.IsSingleFileRelease) { return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id) - .OrderBy(x => x.ReleaseDate) - .ToList(), includeExisting); + .OrderBy(x => x.ReleaseDate) + .ToList(), includeExisting); } // sort candidate releases by closest track count so that we stand a chance of diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs index a2f68fd15..4b53efff4 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs @@ -120,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification var releaseYear = release.ReleaseDate?.Year ?? 0; // The single file version's year is from the album year already, to avoid false positives here we consider it's always different - var isSameWithAlbumYear = (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease) ? false : localYear == albumYear; + var isSameWithAlbumYear = localTracks.All(x => x.IsSingleFileRelease == true) ? false : localYear == albumYear; if (isSameWithAlbumYear || localYear == releaseYear) { dist.Add("year", 0.0); @@ -179,7 +179,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification } // tracks - if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease) + if (localTracks.All(x => x.IsSingleFileRelease == true)) { dist.Add("tracks", 0); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index 32cd6f43a..bf5b76501 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -154,7 +154,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease) { - if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + if (localAlbumRelease.IsSingleFileRelease) { return false; } @@ -340,10 +340,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification localAlbumRelease.AlbumRelease = release; localAlbumRelease.ExistingTracks = extraTracks; localAlbumRelease.TrackMapping = mapping; - if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + if (localAlbumRelease.IsSingleFileRelease) { - localAlbumRelease.LocalTracks[0].Tracks = release.Tracks; - localAlbumRelease.LocalTracks[0].Tracks.ForEach(x => x.IsSingleFileRelease = true); + localAlbumRelease.LocalTracks.ForEach(x => x.Tracks.Clear()); + for (var i = 0; i < release.Tracks.Value.Count; i++) + { + var track = release.Tracks.Value[i]; + var localTrackIndex = localAlbumRelease.LocalTracks.FindIndex(x => x.FileTrackInfo.DiscNumber == track.MediumNumber); + if (localTrackIndex != -1) + { + localAlbumRelease.LocalTracks[localTrackIndex].Tracks.Add(track); + } + } } if (currDistance == 0.0) @@ -360,10 +368,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification public TrackMapping MapReleaseTracks(List localTracks, List mbTracks) { var result = new TrackMapping(); - if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease) + result.IsSingleFileRelease = localTracks.All(x => x.IsSingleFileRelease == true); + if (result.IsSingleFileRelease) { - result.IsSingleFileRelease = true; - return result; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 4ec9e6012..ae3ccc650 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using NLog; using NzbDrone.Common; using NzbDrone.Common.Crypto; @@ -134,33 +132,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual return ProcessFolder(path, downloadId, artist, filter, replaceExistingFiles); } - private static List ReadFieldFromCuesheet(string[] lines, string fieldName) - { - var results = new List(); - var candidates = lines.Where(l => l.StartsWith(fieldName)).ToList(); - foreach (var candidate in candidates) - { - var matches = Regex.Matches(candidate, "\"(.*?)\""); - var result = matches.ToList()[0].Groups[1].Value; - results.Add(result); - } - - return results; - } - - private static string ReadOptionalFieldFromCuesheet(string[] lines, string fieldName) - { - var results = lines.Where(l => l.StartsWith(fieldName)); - if (results.Any()) - { - var matches = Regex.Matches(results.ToList()[0], fieldName + " (.+)"); - var result = matches.ToList()[0].Groups[1].Value; - return result; - } - - return ""; - } - private List ProcessFolder(string folder, string downloadId, Artist artist, FilterFilesType filter, bool replaceExistingFiles) { DownloadClientItem downloadClientItem = null; @@ -186,65 +157,31 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual audioFiles.RemoveAll(l => cueFiles.Contains(l)); foreach (var cueFile in cueFiles) { - // TODO move this to the disk service - using (var fs = cueFile.OpenRead()) - { - var bytes = new byte[cueFile.Length]; - var encoding = new UTF8Encoding(true); - string content; - while (fs.Read(bytes, 0, bytes.Length) > 0) - { - content = encoding.GetString(bytes); - var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + var cueSheet = new CueSheet(cueFile); - // Single-file cue means it's an unsplit image - var fileNames = ReadFieldFromCuesheet(lines, "FILE"); - if (fileNames.Empty() || fileNames.Count > 1) - { - continue; - } + Artist artistFromCue = null; + if (!cueSheet.Performer.Empty()) + { + artistFromCue = _parsingService.GetArtist(cueSheet.Performer); + } - var fileName = fileNames[0]; - if (!fileName.Empty()) - { - Artist artistFromCue = null; - var artistNames = ReadFieldFromCuesheet(lines, "PERFORMER"); - if (artistNames.Count > 0) - { - artistFromCue = _parsingService.GetArtist(artistNames[0]); - } - - string albumTitle = null; - var albumTitles = ReadFieldFromCuesheet(lines, "TITLE"); - if (artistNames.Count > 0) - { - albumTitle = albumTitles[0]; - } - - var date = ReadOptionalFieldFromCuesheet(lines, "REM DATE"); - var audioFile = audioFiles.Find(x => x.Name == fileName && x.DirectoryName == cueFile.DirectoryName); - var parsedAlbumInfo = new ParsedAlbumInfo - { - AlbumTitle = albumTitle, - ArtistName = artistFromCue.Name, - ReleaseDate = date, - }; - var albumsFromCue = _parsingService.GetAlbums(parsedAlbumInfo, artistFromCue); - if (albumsFromCue == null || albumsFromCue.Count == 0) - { - continue; - } - - var tempAudioFiles = new List - { - audioFile - }; - - results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, cueFile.FullName)); - audioFiles.Remove(audioFile); - } - } + var audioFile = audioFiles.Find(x => x.Name == cueSheet.FileName && x.DirectoryName == cueFile.DirectoryName); + var parsedAlbumInfo = new ParsedAlbumInfo + { + AlbumTitle = cueSheet.Title, + ArtistName = artistFromCue.Name, + ReleaseDate = cueSheet.Date, + }; + var albumsFromCue = _parsingService.GetAlbums(parsedAlbumInfo, artistFromCue); + if (albumsFromCue == null || albumsFromCue.Count == 0) + { + continue; } + + var tempAudioFiles = new List { audioFile }; + + results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, cueSheet.Title, tempAudioFiles, cueFile.FullName)); + audioFiles.Remove(audioFile); } results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, string.Empty)); @@ -322,7 +259,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual IncludeExisting = !replaceExistingFiles, AddNewArtists = false }; - var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, null, config); + + var itemInfo = new ImportDecisionMakerInfo + { + IsSingleFileRelease = group.All(x => x.IsSingleFileRelease == true) + }; + + // TODO support with the cuesheet + var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, itemInfo, config); var existingItems = group.Join(decisions, i => i.Path, diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs index 81743a89b..011f9197c 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs @@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications { double dist; string reasons; - if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + if (item.IsSingleFileRelease) { _logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}"); return Decision.Accept(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs index 64c88437a..816d49aa2 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { - if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + if (item.IsSingleFileRelease) { return Decision.Accept(); } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs index 9ff31e479..6a614708f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { - if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + if (item.IsSingleFileRelease) { return Decision.Accept(); } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f3a5b31ab..a6d0ba6f5 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -105,16 +105,13 @@ namespace NzbDrone.Core.Organizer var pattern = namingConfig.StandardTrackFormat; - if (!trackFile.IsSingleFileRelease) + if (tracks.First().AlbumRelease.Value.Media.Count > 1) { - if (tracks.First().AlbumRelease.Value.Media.Count > 1) - { - pattern = namingConfig.MultiDiscTrackFormat; - } - - tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); + pattern = namingConfig.MultiDiscTrackFormat; } + tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); + var splitPatterns = pattern.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); var components = new List(); @@ -126,14 +123,14 @@ namespace NzbDrone.Core.Organizer if (!trackFile.IsSingleFileRelease) { splitPattern = FormatTrackNumberTokens(splitPattern, "", tracks); - splitPattern = FormatMediumNumberTokens(splitPattern, "", tracks); } + splitPattern = FormatMediumNumberTokens(splitPattern, "", tracks); AddArtistTokens(tokenHandlers, artist); AddAlbumTokens(tokenHandlers, album); + AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber)); if (!trackFile.IsSingleFileRelease) { - AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber)); AddTrackTokens(tokenHandlers, tracks, artist); AddTrackTitlePlaceholderTokens(tokenHandlers); AddTrackFileTokens(tokenHandlers, trackFile); diff --git a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs index 27424e04d..0d1dd69c6 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs @@ -61,6 +61,8 @@ namespace NzbDrone.Core.Parser.Model { return "[" + string.Join(", ", LocalTracks.Select(x => Path.GetDirectoryName(x.Path)).Distinct()) + "]"; } + + public bool IsSingleFileRelease => LocalTracks.All(x => x.IsSingleFileRelease == true); } public class TrackMapping