From 31016bca8a555e4de2004a939231571ceb0be102 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 1 Oct 2023 23:01:43 +0200 Subject: [PATCH 01/20] Add support to read cuesheet file and import a single-file release. (cherry picked from commit 506e4415d613d3752605131d0f8b63fa448ee696) --- frontend/src/Album/Details/TrackRow.js | 4 +- .../src/Album/Details/TrackRowConnector.js | 3 +- .../InteractiveImportModalContent.js | 6 + .../InteractiveImportModalContentConnector.js | 4 +- .../Interactive/InteractiveImportRow.js | 21 ++- .../Store/Actions/interactiveImportActions.js | 1 + .../ManualImport/ManualImportController.cs | 3 +- .../ManualImport/ManualImportResource.cs | 2 + .../ManualImportUpdateResource.cs | 1 + src/Lidarr.Api.V1/Tracks/TrackResource.cs | 2 + .../Datastore/Migration/073_add_flac_cue.cs | 14 ++ .../MediaFiles/MediaFileExtensions.cs | 3 +- src/NzbDrone.Core/MediaFiles/TrackFile.cs | 1 + .../Aggregators/AggregateFilenameInfo.cs | 21 ++- .../Identification/CandidateService.cs | 7 + .../Identification/DistanceCalculator.cs | 44 ++++--- .../Identification/IdentificationService.cs | 20 ++- .../TrackImport/ImportApprovedTracks.cs | 3 +- .../TrackImport/ImportDecisionMaker.cs | 10 +- .../TrackImport/Manual/ManualImportFile.cs | 1 + .../TrackImport/Manual/ManualImportItem.cs | 1 + .../TrackImport/Manual/ManualImportService.cs | 124 +++++++++++++++++- .../CloseAlbumMatchSpecification.cs | 6 + .../CloseTrackMatchSpecification.cs | 5 + .../Specifications/MoreTracksSpecification.cs | 5 + ...NoMissingOrUnmatchedTracksSpecification.cs | 5 + src/NzbDrone.Core/Music/Model/Track.cs | 3 + .../Organizer/FileNameBuilder.cs | 38 ++++-- .../Parser/Model/LocalAlbumRelease.cs | 1 + src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 2 +- 30 files changed, 308 insertions(+), 53 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs diff --git a/frontend/src/Album/Details/TrackRow.js b/frontend/src/Album/Details/TrackRow.js index 5f60df882..e79672860 100644 --- a/frontend/src/Album/Details/TrackRow.js +++ b/frontend/src/Album/Details/TrackRow.js @@ -28,6 +28,7 @@ class TrackRow extends Component { absoluteTrackNumber, title, duration, + isSingleFileRelease, trackFilePath, trackFileSize, customFormats, @@ -86,7 +87,7 @@ class TrackRow extends Component { return ( { - trackFilePath + isSingleFileRelease ? `${trackFilePath} (Single File)` : trackFilePath } ); @@ -203,6 +204,7 @@ TrackRow.propTypes = { absoluteTrackNumber: PropTypes.number, title: PropTypes.string.isRequired, duration: PropTypes.number.isRequired, + isSingleFileRelease: PropTypes.bool.isRequired, isSaving: PropTypes.bool, trackFilePath: PropTypes.string, trackFileSize: PropTypes.number, diff --git a/frontend/src/Album/Details/TrackRowConnector.js b/frontend/src/Album/Details/TrackRowConnector.js index aee72e39a..37f4fc00a 100644 --- a/frontend/src/Album/Details/TrackRowConnector.js +++ b/frontend/src/Album/Details/TrackRowConnector.js @@ -13,7 +13,8 @@ function createMapStateToProps() { trackFilePath: trackFile ? trackFile.path : null, trackFileSize: trackFile ? trackFile.size : null, customFormats: trackFile ? trackFile.customFormats : [], - customFormatScore: trackFile ? trackFile.customFormatScore : 0 + customFormatScore: trackFile ? trackFile.customFormatScore : 0, + isSingleFileRelease: trackFile ? trackFile.isSingleFileRelease : false }; } ); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index d1361a785..220b13a4b 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -53,6 +53,11 @@ const columns = [ label: () => translate('Tracks'), isVisible: true }, + { + name: 'isSingleFileRelease', + label: () => 'Is Single File Release', + isVisible: true + }, { name: 'releaseGroup', label: () => translate('ReleaseGroup'), @@ -435,6 +440,7 @@ class InteractiveImportModalContent extends Component { allowArtistChange={allowArtistChange} onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} + isSingleFileRelease={item.isSingleFileRelease} /> ); }) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index f40da69ee..d05b38e06 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component { album, albumReleaseId, tracks, + isSingleFileRelease, quality, disableReleaseSwitching } = item; @@ -148,7 +149,7 @@ class InteractiveImportModalContentConnector extends Component { return false; } - if (!tracks || !tracks.length) { + if (!isSingleFileRelease && (!tracks || !tracks.length)) { this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' }); return false; } @@ -164,6 +165,7 @@ class InteractiveImportModalContentConnector extends Component { albumId: album.id, albumReleaseId, trackIds: _.map(tracks, 'id'), + isSingleFileRelease: item.isSingleFileRelease, quality, downloadId: this.props.downloadId, disableReleaseSwitching diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index b914c0996..797ad389e 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -64,6 +64,7 @@ class InteractiveImportRow extends Component { artist, album, tracks, + isSingleFileRelease, quality, isSelected, onValidRowChange @@ -82,7 +83,7 @@ class InteractiveImportRow extends Component { const isValid = !!( artist && album && - tracks.length && + (isSingleFileRelease || tracks.length) && quality ); @@ -167,6 +168,7 @@ class InteractiveImportRow extends Component { album, albumReleaseId, tracks, + isSingleFileRelease, quality, releaseGroup, size, @@ -257,7 +259,7 @@ class InteractiveImportRow extends Component { @@ -265,10 +267,20 @@ class InteractiveImportRow extends Component { showTrackNumbersLoading && } { - showTrackNumbersPlaceholder ? : trackNumbers + !isSingleFileRelease && showTrackNumbersPlaceholder ? : trackNumbers } + + + { + isSingleFileRelease ? 'Yes' : 'No' + } + + e.id), + isSingleFileRelease: item.isSingleFileRelease, quality: item.quality, releaseGroup: item.releaseGroup, downloadId: item.downloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs index b11c36a91..071fbe05f 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs @@ -83,7 +83,8 @@ namespace Lidarr.Api.V1.ManualImport DownloadId = resource.DownloadId, AdditionalFile = resource.AdditionalFile, ReplaceExistingFiles = resource.ReplaceExistingFiles, - DisableReleaseSwitching = resource.DisableReleaseSwitching + DisableReleaseSwitching = resource.DisableReleaseSwitching, + IsSingleFileRelease = resource.IsSingleFileRelease, }); } diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs index 4b38b4f7c..5d4f29815 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs @@ -29,6 +29,7 @@ namespace Lidarr.Api.V1.ManualImport public bool AdditionalFile { get; set; } public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } + public bool IsSingleFileRelease { get; set; } } public static class ManualImportResourceMapper @@ -52,6 +53,7 @@ namespace Lidarr.Api.V1.ManualImport Tracks = model.Tracks.ToResource(), Quality = model.Quality, ReleaseGroup = model.ReleaseGroup, + IsSingleFileRelease = model.IsSingleFileRelease, // QualityWeight DownloadId = model.DownloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs index 84a513807..bf19a2b1b 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -21,6 +21,7 @@ namespace Lidarr.Api.V1.ManualImport public bool AdditionalFile { get; set; } public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } + public bool IsSingleFileRelease { get; set; } public IEnumerable Rejections { get; set; } } diff --git a/src/Lidarr.Api.V1/Tracks/TrackResource.cs b/src/Lidarr.Api.V1/Tracks/TrackResource.cs index 47f58811d..59834b273 100644 --- a/src/Lidarr.Api.V1/Tracks/TrackResource.cs +++ b/src/Lidarr.Api.V1/Tracks/TrackResource.cs @@ -26,6 +26,7 @@ namespace Lidarr.Api.V1.Tracks public ArtistResource Artist { get; set; } public Ratings Ratings { get; set; } + public bool IsSingleFileRelease { get; set; } // Hiding this so people don't think its usable (only used to set the initial state) [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] @@ -58,6 +59,7 @@ namespace Lidarr.Api.V1.Tracks MediumNumber = model.MediumNumber, HasFile = model.HasFile, Ratings = model.Ratings, + IsSingleFileRelease = model.IsSingleFileRelease }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs b/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs new file mode 100644 index 000000000..d4ad6b928 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(073)] + public class add_flac_cue : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("TrackFiles").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs index b87fcc619..8833ba94d 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileExtensions.cs @@ -27,7 +27,8 @@ namespace NzbDrone.Core.MediaFiles { ".ape", Quality.APE }, { ".aif", Quality.Unknown }, { ".aiff", Quality.Unknown }, - { ".aifc", Quality.Unknown } + { ".aifc", Quality.Unknown }, + { ".cue", Quality.Unknown } }; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index de5d8a805..07eb8a694 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.MediaFiles public QualityModel Quality { get; set; } public MediaInfoModel MediaInfo { get; set; } public int AlbumId { get; set; } + public bool IsSingleFileRelease { get; set; } // These are queried from the database public LazyLoaded> Tracks { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs index 28c2c3633..9afb5e927 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs @@ -65,14 +65,25 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators || tracks.Any(x => x.FileTrackInfo.DiscNumber == 0)) { _logger.Debug("Missing data in tags, trying filename augmentation"); - foreach (var charSep in CharsAndSeps) + if (tracks.Count == 1 && tracks[0].IsSingleFileRelease) { - foreach (var pattern in Patterns(charSep.Item1, charSep.Item2)) + tracks[0].FileTrackInfo.ArtistTitle = tracks[0].Artist.Name; + tracks[0].FileTrackInfo.AlbumTitle = tracks[0].Album.Title; + + // 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; + } + else + { + foreach (var charSep in CharsAndSeps) { - var matches = AllMatches(tracks, pattern); - if (matches != null) + foreach (var pattern in Patterns(charSep.Item1, charSep.Item2)) { - ApplyMatches(matches, pattern); + var matches = AllMatches(tracks, pattern); + if (matches != null) + { + ApplyMatches(matches, pattern); + } } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs index 6fb62bff0..1d3e0e140 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/CandidateService.cs @@ -131,6 +131,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification private List GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting) { + if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + { + return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id) + .OrderBy(x => x.ReleaseDate) + .ToList(), includeExisting); + } + // sort candidate releases by closest track count so that we stand a chance of // getting a perfect match early on return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs index 833ccb12f..a2f68fd15 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs @@ -118,13 +118,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0; var releaseYear = release.ReleaseDate?.Year ?? 0; - if (localYear == albumYear || localYear == releaseYear) + + // 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; + if (isSameWithAlbumYear || localYear == releaseYear) { dist.Add("year", 0.0); } else { - var remoteYear = albumYear > 0 ? albumYear : releaseYear; + var remoteYear = (albumYear > 0 && isSameWithAlbumYear) ? albumYear : releaseYear; var diff = Math.Abs(localYear - remoteYear); var diff_max = Math.Abs(DateTime.Now.Year - remoteYear); dist.AddRatio("year", diff, diff_max); @@ -176,28 +179,35 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification } // tracks - foreach (var pair in mapping.Mapping) + if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease) { - dist.Add("tracks", pair.Value.Item2.NormalizedDistance()); + dist.Add("tracks", 0); } + else + { + foreach (var pair in mapping.Mapping) + { + dist.Add("tracks", pair.Value.Item2.NormalizedDistance()); + } - Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance()); + Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance()); - // missing tracks - foreach (var track in mapping.MBExtra.Take(localTracks.Count)) - { - dist.Add("missing_tracks", 1.0); - } + // missing tracks + foreach (var track in mapping.MBExtra.Take(localTracks.Count)) + { + dist.Add("missing_tracks", 1.0); + } - Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance()); + Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance()); - // unmatched tracks - foreach (var track in mapping.LocalExtra.Take(localTracks.Count)) - { - dist.Add("unmatched_tracks", 1.0); - } + // unmatched tracks + foreach (var track in mapping.LocalExtra.Take(localTracks.Count)) + { + dist.Add("unmatched_tracks", 1.0); + } - Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance()); + Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance()); + } return dist; } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index ccd0b3939..32cd6f43a 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -154,6 +154,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease) { + if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + { + return false; + } + var worstTrackMatchDist = localAlbumRelease.TrackMapping?.Mapping .DefaultIfEmpty() .MaxBy(x => x.Value.Item2.NormalizedDistance()) @@ -335,6 +340,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification localAlbumRelease.AlbumRelease = release; localAlbumRelease.ExistingTracks = extraTracks; localAlbumRelease.TrackMapping = mapping; + if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) + { + localAlbumRelease.LocalTracks[0].Tracks = release.Tracks; + localAlbumRelease.LocalTracks[0].Tracks.ForEach(x => x.IsSingleFileRelease = true); + } + if (currDistance == 0.0) { break; @@ -348,6 +359,14 @@ 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 = true; + + return result; + } + var distances = new Distance[localTracks.Count, mbTracks.Count]; var costs = new double[localTracks.Count, mbTracks.Count]; @@ -364,7 +383,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification var m = new Munkres(costs); m.Run(); - var result = new TrackMapping(); foreach (var pair in m.Solution) { result.Mapping.Add(localTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2])); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index ba54e10c8..535751ca7 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -194,7 +194,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport AlbumId = localTrack.Album.Id, Artist = localTrack.Artist, Album = localTrack.Album, - Tracks = localTrack.Tracks + Tracks = localTrack.Tracks, + IsSingleFileRelease = localTrack.IsSingleFileRelease, }; bool copyOnly; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index aae1a9e6f..b047d3770 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO.Abstractions; using System.Linq; +using DryIoc.ImTools; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; @@ -32,6 +33,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { public DownloadClientItem DownloadClientItem { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } + public bool IsSingleFileRelease { get; set; } } public class ImportDecisionMakerConfig @@ -149,6 +151,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var decisions = trackData.Item2; localTracks.ForEach(x => x.ExistingFile = !config.NewDownload); + localTracks.ForEach(x => x.IsSingleFileRelease = itemInfo.IsSingleFileRelease); + if (itemInfo.IsSingleFileRelease) + { + localTracks.ForEach(x => x.Artist = idOverrides.Artist); + localTracks.ForEach(x => x.Album = idOverrides.Album); + } var releases = _identificationService.Identify(localTracks, idOverrides, config); @@ -246,7 +254,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { ImportDecision decision = null; - if (localTrack.Tracks.Empty()) + if (!localTrack.IsSingleFileRelease && localTrack.Tracks.Empty()) { decision = localTrack.Album != null ? new ImportDecision(localTrack, new Rejection($"Couldn't parse track from: {localTrack.FileTrackInfo}")) : new ImportDecision(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}")); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index 9faec9a65..631318c89 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public QualityModel Quality { get; set; } public string DownloadId { get; set; } public bool DisableReleaseSwitching { get; set; } + public bool IsSingleFileRelease { get; set; } public bool Equals(ManualImportFile other) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index b96fbc045..879eff917 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -32,5 +32,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public bool AdditionalFile { get; set; } public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } + public bool IsSingleFileRelease { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 0b8a18f6c..9e6308323 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -3,6 +3,8 @@ 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; @@ -132,6 +134,33 @@ 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; @@ -149,15 +178,91 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual } } - var artistFiles = _diskScanService.GetAudioFiles(folder).ToList(); + var audioFiles = _diskScanService.GetAudioFiles(folder).ToList(); + var results = new List(); + + // Split cue and non-cue files + var cueFiles = audioFiles.Where(x => x.Extension.Equals(".cue")).ToList(); + audioFiles.RemoveAll(l => cueFiles.Contains(l)); + foreach (var cueFile in cueFiles) + { + 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); + + // Single-file cue means it's an unsplit image + var fileNames = ReadFieldFromCuesheet(lines, "FILE"); + if (fileNames.Empty() || fileNames.Count > 1) + { + continue; + } + + 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, true)); + audioFiles.Remove(audioFile); + } + } + } + } + + results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, false)); + + return results; + } + + private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, bool isSingleFileRelease) + { var idOverrides = new IdentificationOverrides { - Artist = artist + Artist = overrideArtist, + Album = overrideAlbum }; var itemInfo = new ImportDecisionMakerInfo { DownloadClientItem = downloadClientItem, - ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(directoryInfo.Name) + ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle), + IsSingleFileRelease = isSingleFileRelease }; var config = new ImportDecisionMakerConfig { @@ -168,10 +273,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AddNewArtists = false }; - var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, idOverrides, itemInfo, config); + var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config); // paths will be different for new and old files which is why we need to map separately - var newFiles = artistFiles.Join(decisions, + var newFiles = audioFiles.Join(decisions, f => f.FullName, d => d.Item.Path, (f, d) => new { File = f, Decision = d }, @@ -299,6 +404,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual item.AdditionalFile = decision.Item.AdditionalFile; item.ReplaceExistingFiles = replaceExistingFiles; item.DisableReleaseSwitching = disableReleaseSwitching; + item.IsSingleFileRelease = decision.Item.IsSingleFileRelease; return item; } @@ -346,9 +452,15 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual Quality = file.Quality, Artist = artist, Album = album, - Release = release + Release = release, + IsSingleFileRelease = file.IsSingleFileRelease, }; + if (file.IsSingleFileRelease) + { + localTrack.Tracks.ForEach(x => x.IsSingleFileRelease = true); + } + var importDecision = new ImportDecision(localTrack); if (_rootFolderService.GetBestRootFolder(artist.Path) == null) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs index fda5da995..81743a89b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseAlbumMatchSpecification.cs @@ -22,11 +22,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications { double dist; string reasons; + if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + { + _logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}"); + return Decision.Accept(); + } // strict when a new download if (item.NewDownload) { dist = item.Distance.NormalizedDistance(); + reasons = item.Distance.Reasons; if (dist > _albumThreshold) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs index 11ffe237a..e817d339e 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/CloseTrackMatchSpecification.cs @@ -17,6 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem) { + if (item.IsSingleFileRelease) + { + return Decision.Accept(); + } + var dist = item.Distance.NormalizedDistance(); var reasons = item.Distance.Reasons; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs index 36454d3f2..64c88437a 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/MoreTracksSpecification.cs @@ -17,6 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { + if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + { + return Decision.Accept(); + } + var existingRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored); var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile); if (item.AlbumRelease.Id != existingRelease.Id && diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs index d54fe89e9..9ff31e479 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/NoMissingOrUnmatchedTracksSpecification.cs @@ -16,6 +16,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) { + if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) + { + return Decision.Accept(); + } + if (item.NewDownload && item.TrackMapping.LocalExtra.Count > 0) { _logger.Debug("This release has track files that have not been matched. Skipping {0}", item); diff --git a/src/NzbDrone.Core/Music/Model/Track.cs b/src/NzbDrone.Core/Music/Model/Track.cs index 9c1aed1b1..57ea8f0ca 100644 --- a/src/NzbDrone.Core/Music/Model/Track.cs +++ b/src/NzbDrone.Core/Music/Model/Track.cs @@ -30,6 +30,7 @@ namespace NzbDrone.Core.Music public Ratings Ratings { get; set; } public int MediumNumber { get; set; } public int TrackFileId { get; set; } + public bool IsSingleFileRelease { get; set; } [MemberwiseEqualityIgnore] public bool HasFile => TrackFileId > 0; @@ -73,6 +74,7 @@ namespace NzbDrone.Core.Music Explicit = other.Explicit; Ratings = other.Ratings; MediumNumber = other.MediumNumber; + IsSingleFileRelease = other.IsSingleFileRelease; } public override void UseDbFieldsFrom(Track other) @@ -81,6 +83,7 @@ namespace NzbDrone.Core.Music AlbumReleaseId = other.AlbumReleaseId; ArtistMetadataId = other.ArtistMetadataId; TrackFileId = other.TrackFileId; + IsSingleFileRelease = other.IsSingleFileRelease; } } } diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index c8cb22a64..f3a5b31ab 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -105,12 +105,15 @@ namespace NzbDrone.Core.Organizer var pattern = namingConfig.StandardTrackFormat; - if (tracks.First().AlbumRelease.Value.Media.Count > 1) + if (!trackFile.IsSingleFileRelease) { - pattern = namingConfig.MultiDiscTrackFormat; - } + if (tracks.First().AlbumRelease.Value.Media.Count > 1) + { + pattern = namingConfig.MultiDiscTrackFormat; + } - tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); + tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList(); + } var splitPatterns = pattern.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); var components = new List(); @@ -119,15 +122,23 @@ namespace NzbDrone.Core.Organizer { var splitPattern = splitPatterns[i]; var tokenHandlers = new Dictionary>(FileNameBuilderTokenEqualityComparer.Instance); - splitPattern = FormatTrackNumberTokens(splitPattern, "", tracks); - splitPattern = FormatMediumNumberTokens(splitPattern, "", tracks); + + if (!trackFile.IsSingleFileRelease) + { + splitPattern = FormatTrackNumberTokens(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)); - AddTrackTokens(tokenHandlers, tracks, artist); - AddTrackTitlePlaceholderTokens(tokenHandlers); - AddTrackFileTokens(tokenHandlers, trackFile); + 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); + } + AddQualityTokens(tokenHandlers, artist, trackFile); AddMediaInfoTokens(tokenHandlers, trackFile); AddCustomFormats(tokenHandlers, artist, trackFile, customFormats); @@ -141,9 +152,12 @@ namespace NzbDrone.Core.Organizer var maxTrackTitleLength = maxPathSegmentLength - GetLengthWithoutTrackTitle(component, namingConfig); - AddTrackTitleTokens(tokenHandlers, tracks, maxTrackTitleLength); - component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim(); + if (!trackFile.IsSingleFileRelease) + { + AddTrackTitleTokens(tokenHandlers, tracks, maxTrackTitleLength); + } + component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim(); component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString()); component = TrimSeparatorsRegex.Replace(component, string.Empty); component = component.Replace("{ellipsis}", "..."); diff --git a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs index 1cc4b69ae..27424e04d 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs @@ -73,5 +73,6 @@ namespace NzbDrone.Core.Parser.Model public Dictionary> Mapping { get; set; } public List LocalExtra { get; set; } public List MBExtra { get; set; } + public bool IsSingleFileRelease { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 1fc38c6ef..e5b48152e 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Parser.Model public bool SceneSource { get; set; } public string ReleaseGroup { get; set; } public string SceneName { get; set; } - + public bool IsSingleFileRelease { get; set; } public override string ToString() { return Path; From 16a3fbe25bb5d839102551e738958846325442f1 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Mon, 2 Oct 2023 21:57:30 +0200 Subject: [PATCH 02/20] Add the support to copy .cue files to the media library folder. (cherry picked from commit 57ae74b49afea29100882edbe6e49fa24210bbbf) --- .../Interactive/InteractiveImportModalContent.js | 6 ++++++ .../InteractiveImportModalContentConnector.js | 4 +++- .../Interactive/InteractiveImportRow.js | 14 +++++++++++++- .../Store/Actions/interactiveImportActions.js | 1 + .../ManualImport/ManualImportController.cs | 1 + .../ManualImport/ManualImportResource.cs | 2 ++ .../ManualImport/ManualImportUpdateResource.cs | 2 +- src/NzbDrone.Core/MediaFiles/DiskScanService.cs | 1 + .../MediaFiles/TrackFileMovingService.cs | 16 ++++++++++++++++ .../TrackImport/Manual/ManualImportFile.cs | 1 + .../TrackImport/Manual/ManualImportItem.cs | 1 + .../TrackImport/Manual/ManualImportService.cs | 16 +++++++++++----- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 1 + 13 files changed, 58 insertions(+), 8 deletions(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index 220b13a4b..d90ba63b9 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -58,6 +58,11 @@ const columns = [ label: () => 'Is Single File Release', isVisible: true }, + { + name: 'cuesheetPath', + label: () => 'Cuesheet Path', + isVisible: true + }, { name: 'releaseGroup', label: () => translate('ReleaseGroup'), @@ -441,6 +446,7 @@ class InteractiveImportModalContent extends Component { onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} isSingleFileRelease={item.isSingleFileRelease} + cuesheetPath={item.cuesheetPath} /> ); }) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index d05b38e06..f7d094733 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -135,6 +135,7 @@ class InteractiveImportModalContentConnector extends Component { albumReleaseId, tracks, isSingleFileRelease, + cuesheetPath, quality, disableReleaseSwitching } = item; @@ -149,7 +150,7 @@ class InteractiveImportModalContentConnector extends Component { return false; } - if (!isSingleFileRelease && (!tracks || !tracks.length)) { + if (!(isSingleFileRelease && cuesheetPath) && (!tracks || !tracks.length)) { this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' }); return false; } @@ -166,6 +167,7 @@ class InteractiveImportModalContentConnector extends Component { albumReleaseId, trackIds: _.map(tracks, 'id'), isSingleFileRelease: item.isSingleFileRelease, + cuesheetPath: item.cuesheetPath, quality, downloadId: this.props.downloadId, disableReleaseSwitching diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 797ad389e..f8d797d53 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -65,6 +65,7 @@ class InteractiveImportRow extends Component { album, tracks, isSingleFileRelease, + cuesheetPath, quality, isSelected, onValidRowChange @@ -83,7 +84,7 @@ class InteractiveImportRow extends Component { const isValid = !!( artist && album && - (isSingleFileRelease || tracks.length) && + ((isSingleFileRelease && cuesheetPath) || tracks.length) && quality ); @@ -169,6 +170,7 @@ class InteractiveImportRow extends Component { albumReleaseId, tracks, isSingleFileRelease, + cuesheetPath, quality, releaseGroup, size, @@ -281,6 +283,15 @@ class InteractiveImportRow extends Component { } + + { + cuesheetPath + } + + e.id), isSingleFileRelease: item.isSingleFileRelease, + cuesheetPath: item.cuesheetPath, quality: item.quality, releaseGroup: item.releaseGroup, downloadId: item.downloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs index 071fbe05f..baafd6468 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs @@ -85,6 +85,7 @@ namespace Lidarr.Api.V1.ManualImport ReplaceExistingFiles = resource.ReplaceExistingFiles, DisableReleaseSwitching = resource.DisableReleaseSwitching, IsSingleFileRelease = resource.IsSingleFileRelease, + CuesheetPath = resource.CuesheetPath, }); } diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs index 5d4f29815..4f132d3f9 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs @@ -30,6 +30,7 @@ namespace Lidarr.Api.V1.ManualImport public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } + public string CuesheetPath { get; set; } } public static class ManualImportResourceMapper @@ -54,6 +55,7 @@ namespace Lidarr.Api.V1.ManualImport Quality = model.Quality, ReleaseGroup = model.ReleaseGroup, IsSingleFileRelease = model.IsSingleFileRelease, + CuesheetPath = model.CuesheetPath, // QualityWeight DownloadId = model.DownloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs index bf19a2b1b..0a443f715 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -22,7 +22,7 @@ namespace Lidarr.Api.V1.ManualImport public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } - + public string CuesheetPath { get; set; } public IEnumerable Rejections { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 0c5c4ca05..f5e6bb5cc 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -140,6 +140,7 @@ namespace NzbDrone.Core.MediaFiles CleanMediaFiles(folder, files.Select(x => x.FullName).ToList()); mediaFileList.AddRange(files); + mediaFileList.RemoveAll(x => x.Extension == ".cue"); } musicFilesStopwatch.Stop(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index fe48b4d6d..9abcf26de 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -4,6 +4,7 @@ using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.TrackImport; @@ -90,6 +91,21 @@ namespace NzbDrone.Core.MediaFiles EnsureTrackFolder(trackFile, localTrack, filePath); + if (!localTrack.CuesheetPath.Empty()) + { + var directory = Path.GetDirectoryName(filePath); + var fileName = Path.GetFileNameWithoutExtension(filePath); + var cuesheetPath = Path.Combine(directory, fileName + ".cue"); + _diskTransferService.TransferFile(localTrack.CuesheetPath, cuesheetPath, TransferMode.Copy); + var lines = new List(File.ReadAllLines(cuesheetPath)); + var fileLineIndex = lines.FindIndex(line => line.Contains("FILE")); + if (fileLineIndex != -1) + { + lines[fileLineIndex] = "FILE \"" + Path.GetFileName(filePath) + "\" WAVE"; + File.WriteAllLines(cuesheetPath, lines); + } + } + if (_configService.CopyUsingHardlinks) { _logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index 631318c89..f3d6cd830 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public string DownloadId { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } + public string CuesheetPath { get; set; } public bool Equals(ManualImportFile other) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index 879eff917..6102d8ad2 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -33,5 +33,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } + public string CuesheetPath { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 9e6308323..4ec9e6012 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -186,6 +186,7 @@ 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]; @@ -239,19 +240,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual audioFile }; - results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, true)); + results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, cueFile.FullName)); audioFiles.Remove(audioFile); } } } } - results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, false)); + results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, string.Empty)); return results; } - private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, bool isSingleFileRelease) + private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, string cuesheetPath) { var idOverrides = new IdentificationOverrides { @@ -262,7 +263,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { DownloadClientItem = downloadClientItem, ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle), - IsSingleFileRelease = isSingleFileRelease + IsSingleFileRelease = !cuesheetPath.Empty() }; var config = new ImportDecisionMakerConfig { @@ -286,7 +287,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision)); var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false)); - return newItems.Concat(existingItems).ToList(); + var itemsList = newItems.Concat(existingItems).ToList(); + itemsList.ForEach(item => { item.CuesheetPath = cuesheetPath; }); + + return itemsList; } public List UpdateItems(List items) @@ -405,6 +409,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual item.ReplaceExistingFiles = replaceExistingFiles; item.DisableReleaseSwitching = disableReleaseSwitching; item.IsSingleFileRelease = decision.Item.IsSingleFileRelease; + item.CuesheetPath = decision.Item.CuesheetPath; return item; } @@ -454,6 +459,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual Album = album, Release = release, IsSingleFileRelease = file.IsSingleFileRelease, + CuesheetPath = file.CuesheetPath, }; if (file.IsSingleFileRelease) diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index e5b48152e..ee929c588 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseGroup { get; set; } public string SceneName { get; set; } public bool IsSingleFileRelease { get; set; } + public string CuesheetPath { get; set; } public override string ToString() { return Path; From c87efacb6a559d0cf20bfb410398b9cac8b0e0b8 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Wed, 4 Oct 2023 19:27:49 +0200 Subject: [PATCH 03/20] Add multi-disc support for single file releases. Add a cue sheet class. Enable track selection for single file releases. (cherry picked from commit 430807a3046f8bb4c36301278ff31fc9a1d3987d) --- .../Interactive/InteractiveImportRow.js | 7 +- src/NzbDrone.Core/MediaFiles/CueSheet.cs | 80 ++++++++++++ .../Aggregators/AggregateFilenameInfo.cs | 15 ++- .../Identification/CandidateService.cs | 6 +- .../Identification/DistanceCalculator.cs | 4 +- .../Identification/IdentificationService.cs | 21 ++-- .../TrackImport/Manual/ManualImportService.cs | 116 +++++------------- .../CloseAlbumMatchSpecification.cs | 2 +- .../Specifications/MoreTracksSpecification.cs | 2 +- ...NoMissingOrUnmatchedTracksSpecification.cs | 2 +- .../Organizer/FileNameBuilder.cs | 15 +-- .../Parser/Model/LocalAlbumRelease.cs | 2 + 12 files changed, 153 insertions(+), 119 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/CueSheet.cs 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 From a7f07df588f9e0190ac917d5350caec853e3d418 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 8 Oct 2023 13:29:59 +0200 Subject: [PATCH 04/20] Add multi-file/track support to the cue sheet loader. Rename "cuesheet" to "cue sheet". (cherry picked from commit c9686684ea82e7af8ad203d1bcbb7983adb9e293) --- .../InteractiveImportModalContent.js | 6 +- .../InteractiveImportModalContentConnector.js | 6 +- .../Interactive/InteractiveImportRow.js | 8 +-- .../Store/Actions/interactiveImportActions.js | 2 +- .../ManualImport/ManualImportController.cs | 2 +- .../ManualImport/ManualImportResource.cs | 4 +- .../ManualImportUpdateResource.cs | 2 +- src/NzbDrone.Core/MediaFiles/CueSheet.cs | 56 +++++++++++-------- .../MediaFiles/TrackFileMovingService.cs | 10 ++-- .../TrackImport/ImportDecisionMaker.cs | 1 + .../TrackImport/Manual/ManualImportFile.cs | 2 +- .../TrackImport/Manual/ManualImportItem.cs | 2 +- .../TrackImport/Manual/ManualImportService.cs | 37 ++++++++---- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 2 +- 14 files changed, 82 insertions(+), 58 deletions(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index d90ba63b9..f57dcbe3a 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -59,8 +59,8 @@ const columns = [ isVisible: true }, { - name: 'cuesheetPath', - label: () => 'Cuesheet Path', + name: 'cueSheetPath', + label: () => 'Cue Sheet Path', isVisible: true }, { @@ -446,7 +446,7 @@ class InteractiveImportModalContent extends Component { onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} isSingleFileRelease={item.isSingleFileRelease} - cuesheetPath={item.cuesheetPath} + cueSheetPath={item.cueSheetPath} /> ); }) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index f7d094733..019d7c212 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -135,7 +135,7 @@ class InteractiveImportModalContentConnector extends Component { albumReleaseId, tracks, isSingleFileRelease, - cuesheetPath, + cueSheetPath, quality, disableReleaseSwitching } = item; @@ -150,7 +150,7 @@ class InteractiveImportModalContentConnector extends Component { return false; } - if (!(isSingleFileRelease && cuesheetPath) && (!tracks || !tracks.length)) { + if (!(isSingleFileRelease && cueSheetPath) && (!tracks || !tracks.length)) { this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' }); return false; } @@ -167,7 +167,7 @@ class InteractiveImportModalContentConnector extends Component { albumReleaseId, trackIds: _.map(tracks, 'id'), isSingleFileRelease: item.isSingleFileRelease, - cuesheetPath: item.cuesheetPath, + cueSheetPath: item.cueSheetPath, quality, downloadId: this.props.downloadId, disableReleaseSwitching diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 470553638..a2c05de62 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -169,7 +169,7 @@ class InteractiveImportRow extends Component { albumReleaseId, tracks, isSingleFileRelease, - cuesheetPath, + cueSheetPath, quality, releaseGroup, size, @@ -284,10 +284,10 @@ class InteractiveImportRow extends Component { { - cuesheetPath + cueSheetPath } @@ -432,7 +432,7 @@ InteractiveImportRow.propTypes = { albumReleaseId: PropTypes.number, tracks: PropTypes.arrayOf(PropTypes.object), isSingleFileRelease: PropTypes.bool.isRequired, - cuesheetPath: PropTypes.string.isRequired, + cueSheetPath: PropTypes.string.isRequired, releaseGroup: PropTypes.string, quality: PropTypes.object, size: PropTypes.number.isRequired, diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 2dc63b6d2..037a56c3a 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -207,7 +207,7 @@ export const actionHandlers = handleThunks({ albumReleaseId: item.albumReleaseId ? item.albumReleaseId : undefined, trackIds: (item.tracks || []).map((e) => e.id), isSingleFileRelease: item.isSingleFileRelease, - cuesheetPath: item.cuesheetPath, + cueSheetPath: item.cueSheetPath, quality: item.quality, releaseGroup: item.releaseGroup, downloadId: item.downloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs index baafd6468..6b27dbd38 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportController.cs @@ -85,7 +85,7 @@ namespace Lidarr.Api.V1.ManualImport ReplaceExistingFiles = resource.ReplaceExistingFiles, DisableReleaseSwitching = resource.DisableReleaseSwitching, IsSingleFileRelease = resource.IsSingleFileRelease, - CuesheetPath = resource.CuesheetPath, + CueSheetPath = resource.CueSheetPath, }); } diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs index 4f132d3f9..a410d2537 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportResource.cs @@ -30,7 +30,7 @@ namespace Lidarr.Api.V1.ManualImport public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } - public string CuesheetPath { get; set; } + public string CueSheetPath { get; set; } } public static class ManualImportResourceMapper @@ -55,7 +55,7 @@ namespace Lidarr.Api.V1.ManualImport Quality = model.Quality, ReleaseGroup = model.ReleaseGroup, IsSingleFileRelease = model.IsSingleFileRelease, - CuesheetPath = model.CuesheetPath, + CueSheetPath = model.CueSheetPath, // QualityWeight DownloadId = model.DownloadId, diff --git a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs index 0a443f715..4e9ec7c03 100644 --- a/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs +++ b/src/Lidarr.Api.V1/ManualImport/ManualImportUpdateResource.cs @@ -22,7 +22,7 @@ namespace Lidarr.Api.V1.ManualImport public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } - public string CuesheetPath { get; set; } + public string CueSheetPath { get; set; } public IEnumerable Rejections { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/CueSheet.cs b/src/NzbDrone.Core/MediaFiles/CueSheet.cs index bcf330f58..a22cb6c0a 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheet.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheet.cs @@ -12,6 +12,8 @@ namespace NzbDrone.Core.MediaFiles { public CueSheet(IFileInfo fileInfo) { + Path = fileInfo.FullName; + using (var fs = fileInfo.OpenRead()) { var bytes = new byte[fileInfo.Length]; @@ -23,58 +25,66 @@ namespace NzbDrone.Core.MediaFiles 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]; + FileNames = ReadField(lines, "FILE"); + IsSingleFileRelease = FileNames.Count == 1; - var performers = ReadFieldFromCuesheet(lines, "PERFORMER"); + var performers = ReadField(lines, "PERFORMER"); if (performers.Count > 0) { Performer = performers[0]; } - var titles = ReadFieldFromCuesheet(lines, "TITLE"); + var titles = ReadField(lines, "TITLE"); if (titles.Count > 0) { Title = titles[0]; } - Date = ReadOptionalFieldFromCuesheet(lines, "REM DATE"); + var dates = ReadField(lines, "REM DATE"); + if (dates.Count > 0) + { + Date = dates[0]; + } } } } + public string Path { get; set; } public bool IsSingleFileRelease { get; set; } - public string FileName { get; set; } + public List FileNames { 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) + private static List ReadField(string[] lines, string fieldName) { + var inQuotePattern = "\"(.*?)\""; + var flatPattern = 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); - } + var matches = Regex.Matches(candidate, inQuotePattern).ToList(); + if (matches.Count == 0) + { + matches = Regex.Matches(candidate, flatPattern).ToList(); + } - return results; - } + if (matches.Count == 0) + { + continue; + } - 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; + var groups = matches[0].Groups; + if (groups.Count > 0) + { + var result = groups[1].Value; + results.Add(result); + } } - return ""; + return results; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index 9abcf26de..5872047ba 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -91,18 +91,18 @@ namespace NzbDrone.Core.MediaFiles EnsureTrackFolder(trackFile, localTrack, filePath); - if (!localTrack.CuesheetPath.Empty()) + if (localTrack.IsSingleFileRelease && !localTrack.CueSheetPath.Empty()) { var directory = Path.GetDirectoryName(filePath); var fileName = Path.GetFileNameWithoutExtension(filePath); - var cuesheetPath = Path.Combine(directory, fileName + ".cue"); - _diskTransferService.TransferFile(localTrack.CuesheetPath, cuesheetPath, TransferMode.Copy); - var lines = new List(File.ReadAllLines(cuesheetPath)); + var cueSheetPath = Path.Combine(directory, fileName + ".cue"); + _diskTransferService.TransferFile(localTrack.CueSheetPath, cueSheetPath, TransferMode.Copy); + var lines = new List(File.ReadAllLines(cueSheetPath)); var fileLineIndex = lines.FindIndex(line => line.Contains("FILE")); if (fileLineIndex != -1) { lines[fileLineIndex] = "FILE \"" + Path.GetFileName(filePath) + "\" WAVE"; - File.WriteAllLines(cuesheetPath, lines); + File.WriteAllLines(cueSheetPath, lines); } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index b047d3770..b1bbc4ec6 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -34,6 +34,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public DownloadClientItem DownloadClientItem { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } public bool IsSingleFileRelease { get; set; } + public CueSheet CueSheet { get; set; } } public class ImportDecisionMakerConfig diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs index f3d6cd830..598ae785c 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportFile.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public string DownloadId { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } - public string CuesheetPath { get; set; } + public string CueSheetPath { get; set; } public bool Equals(ManualImportFile other) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs index 6102d8ad2..8456dfa5f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportItem.cs @@ -33,6 +33,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual public bool ReplaceExistingFiles { get; set; } public bool DisableReleaseSwitching { get; set; } public bool IsSingleFileRelease { get; set; } - public string CuesheetPath { get; set; } + public string CueSheetPath { get; set; } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index ae3ccc650..3adf9cd4f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -165,7 +165,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual artistFromCue = _parsingService.GetArtist(cueSheet.Performer); } - var audioFile = audioFiles.Find(x => x.Name == cueSheet.FileName && x.DirectoryName == cueFile.DirectoryName); + if (artistFromCue == null) + { + continue; + } + + // TODO use the audio files from the cue sheet + var validAudioFiles = audioFiles.FindAll(x => cueSheet.FileNames.Contains(x.Name)); + if (validAudioFiles.Count == 0) + { + continue; + } + var parsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = cueSheet.Title, @@ -178,18 +189,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual 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, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, cueSheet.Title, validAudioFiles, cueSheet)); + audioFiles.RemoveAll(x => validAudioFiles.Contains(x)); } - results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, string.Empty)); + results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, null)); return results; } - private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, string cuesheetPath) + private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, CueSheet cueSheet) { var idOverrides = new IdentificationOverrides { @@ -200,7 +209,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { DownloadClientItem = downloadClientItem, ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle), - IsSingleFileRelease = !cuesheetPath.Empty() + CueSheet = cueSheet, + IsSingleFileRelease = cueSheet != null ? cueSheet.IsSingleFileRelease : false, }; var config = new ImportDecisionMakerConfig { @@ -225,7 +235,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false)); var itemsList = newItems.Concat(existingItems).ToList(); - itemsList.ForEach(item => { item.CuesheetPath = cuesheetPath; }); + if (cueSheet != null) + { + itemsList.ForEach(item => { item.CueSheetPath = cueSheet.Path; }); + } return itemsList; } @@ -265,7 +278,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual IsSingleFileRelease = group.All(x => x.IsSingleFileRelease == true) }; - // TODO support with the cuesheet + // TODO support with the cue sheet var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, itemInfo, config); var existingItems = group.Join(decisions, @@ -353,7 +366,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual item.ReplaceExistingFiles = replaceExistingFiles; item.DisableReleaseSwitching = disableReleaseSwitching; item.IsSingleFileRelease = decision.Item.IsSingleFileRelease; - item.CuesheetPath = decision.Item.CuesheetPath; + item.CueSheetPath = decision.Item.CueSheetPath; return item; } @@ -403,7 +416,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual Album = album, Release = release, IsSingleFileRelease = file.IsSingleFileRelease, - CuesheetPath = file.CuesheetPath, + CueSheetPath = file.CueSheetPath, }; if (file.IsSingleFileRelease) diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index ee929c588..422ac2b87 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseGroup { get; set; } public string SceneName { get; set; } public bool IsSingleFileRelease { get; set; } - public string CuesheetPath { get; set; } + public string CueSheetPath { get; set; } public override string ToString() { return Path; From 72b504ff911f1f7eddced6a993f7f692771af33a Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 8 Oct 2023 15:23:32 +0200 Subject: [PATCH 05/20] Fix an issue that "Is Single File Release" column is not added to Tracks table. (cherry picked from commit 2d7e4bafaf4678f5aea1b91df2f44be5ffccd074) --- src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs b/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs index d4ad6b928..0f8b8f5d6 100644 --- a/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs +++ b/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { + Alter.Table("Tracks").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false); Alter.Table("TrackFiles").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false); } } From cb7a7ec24f98045f63311513b5ad8dd5e9413590 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Thu, 12 Oct 2023 00:48:34 +0200 Subject: [PATCH 06/20] Refactor the cue sheet file loader to read the track titles, disc ID and other useful information. Add support to import releases with multiple cue sheets. Add the cue sheet support to the disc scan service. Use the track info from cue sheet files to map local tracks. Use the disc ID to group cue sheet files and deduce the disc count. (cherry picked from commit fac76b7cfb746e05f9924047e698ef41203efe5e) --- src/NzbDrone.Core/MediaFiles/CueSheet.cs | 199 +++++++++++++---- .../MediaFiles/DiskScanService.cs | 89 ++++++-- .../Aggregators/AggregateFilenameInfo.cs | 2 - .../Identification/IdentificationService.cs | 55 +++-- .../TrackImport/ImportDecisionMaker.cs | 109 ++++++++- .../TrackImport/Manual/ManualImportService.cs | 211 ++++++++++-------- 6 files changed, 484 insertions(+), 181 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/CueSheet.cs b/src/NzbDrone.Core/MediaFiles/CueSheet.cs index a22cb6c0a..84374635e 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheet.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheet.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO.Abstractions; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using NzbDrone.Core.Datastore; @@ -23,68 +22,180 @@ namespace NzbDrone.Core.MediaFiles { content = encoding.GetString(bytes); var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + ParseCueSheet(lines); - // Single-file cue means it's an unsplit image - FileNames = ReadField(lines, "FILE"); - IsSingleFileRelease = FileNames.Count == 1; + // Single-file cue means it's an unsplit image, which should be specially treated in the pipeline + IsSingleFileRelease = Files.Count == 1; + } + } + } - var performers = ReadField(lines, "PERFORMER"); - if (performers.Count > 0) - { - Performer = performers[0]; - } + public class IndexEntry + { + public int Key { get; set; } + public string Time { get; set; } + } - var titles = ReadField(lines, "TITLE"); - if (titles.Count > 0) - { - Title = titles[0]; - } + public class TrackEntry + { + public int Number { get; set; } + public string Title { get; set; } + public string Performer { get; set; } + public List Indices { get; set; } = new List(); + } - var dates = ReadField(lines, "REM DATE"); - if (dates.Count > 0) - { - Date = dates[0]; - } - } - } + public class FileEntry + { + public string Name { get; set; } + public IndexEntry Index { get; set; } + public List Tracks { get; set; } = new List(); } public string Path { get; set; } public bool IsSingleFileRelease { get; set; } - public List FileNames { get; set; } + public List Files { get; set; } = new List(); + public string Genre { get; set; } + public string Date { get; set; } + public string DiscID { get; set; } public string Title { get; set; } public string Performer { get; set; } - public string Date { get; set; } + private static string _FileKey = "FILE"; + private static string _TrackKey = "TRACK"; + private static string _IndexKey = "INDEX"; + private static string _GenreKey = "REM GENRE"; + private static string _DateKey = "REM DATE"; + private static string _DiscIdKey = "REM DISCID"; + private static string _PerformerKey = "PERFORMER"; + private static string _TitleKey = "TITLE"; - private static List ReadField(string[] lines, string fieldName) + private string ExtractValue(string line, string keyword) { - var inQuotePattern = "\"(.*?)\""; - var flatPattern = fieldName + " (.+)"; + var pattern = keyword + @"\s+(?:(?:\""(.*?)\"")|(.+))"; + var match = Regex.Match(line, pattern); - var results = new List(); - var candidates = lines.Where(l => l.StartsWith(fieldName)).ToList(); - foreach (var candidate in candidates) + if (match.Success) { - var matches = Regex.Matches(candidate, inQuotePattern).ToList(); - if (matches.Count == 0) - { - matches = Regex.Matches(candidate, flatPattern).ToList(); - } + var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + return value; + } - if (matches.Count == 0) - { - continue; - } + return ""; + } - var groups = matches[0].Groups; - if (groups.Count > 0) + private void ParseCueSheet(string[] lines) + { + var i = 0; + try + { + while (true) { - var result = groups[1].Value; - results.Add(result); + var line = lines[i]; + if (line.StartsWith(_FileKey)) + { + line = line.Trim(); + line = line.Substring(_FileKey.Length).Trim(); + var filename = line.Split('"')[1]; + var fileDetails = new FileEntry { Name = filename }; + + i++; + line = lines[i]; + while (line.StartsWith(" ")) + { + line = line.Trim(); + if (line.StartsWith(_TrackKey)) + { + line = line.Substring(_TrackKey.Length).Trim(); + } + + var trackDetails = new TrackEntry(); + var trackInfo = line.Split(' '); + if (trackInfo.Length > 0) + { + if (int.TryParse(trackInfo[0], out var number)) + { + trackDetails.Number = number; + } + } + + i++; + line = lines[i]; + while (line.StartsWith(" ")) + { + line = line.Trim(); + if (line.StartsWith(_IndexKey)) + { + line = line.Substring(_IndexKey.Length).Trim(); + var parts = line.Split(' '); + if (parts.Length > 1) + { + if (int.TryParse(parts[0], out var key)) + { + var value = parts[1].Trim('"'); + trackDetails.Indices.Add(new IndexEntry { Key = key, Time = value }); + } + } + + i++; + line = lines[i]; + } + else if (line.StartsWith(_TitleKey)) + { + trackDetails.Title = ExtractValue(line, _TitleKey); + i++; + line = lines[i]; + } + else if (line.StartsWith(_PerformerKey)) + { + trackDetails.Performer = ExtractValue(line, _PerformerKey); + i++; + line = lines[i]; + } + else + { + i++; + line = lines[i]; + } + } + + fileDetails.Tracks.Add(trackDetails); + } + + Files.Add(fileDetails); + } + else if (line.StartsWith(_GenreKey)) + { + Genre = ExtractValue(line, _GenreKey); + i++; + } + else if (line.StartsWith(_DateKey)) + { + Date = ExtractValue(line, _DateKey); + i++; + } + else if (line.StartsWith(_DiscIdKey)) + { + DiscID = ExtractValue(line, _DiscIdKey); + i++; + } + else if (line.StartsWith(_PerformerKey)) + { + Performer = ExtractValue(line, _PerformerKey); + i++; + } + else if (line.StartsWith(_TitleKey)) + { + Title = ExtractValue(line, _TitleKey); + i++; + } + else + { + i++; + } } } - - return results; + catch (IndexOutOfRangeException) + { + } } } } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index f5e6bb5cc..145f40cd6 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -11,12 +11,14 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles @@ -83,6 +85,66 @@ namespace NzbDrone.Core.MediaFiles artistIds = new List(); } + var mediaFileList = GetMediaFiles(folders, artistIds); + + var decisionsStopwatch = Stopwatch.StartNew(); + + var itemInfo = new ImportDecisionMakerInfo(); + var config = new ImportDecisionMakerConfig + { + Filter = filter, + IncludeExisting = true, + AddNewArtists = addNewArtists + }; + + var decisions = new List>(); + var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList(); + mediaFileList.RemoveAll(l => cueFiles.Contains(l)); + var cueSheetInfos = new List(); + foreach (var cueFile in cueFiles) + { + var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, mediaFileList); + cueSheetInfos.Add(cueSheetInfo); + } + + var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); + foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) + { + var audioFilesForCues = new List(); + foreach (var cueSheetInfo in cueSheetInfoGroup) + { + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + } + + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfos[0].IdOverrides, itemInfo, config, cueSheetInfos)); + + foreach (var cueSheetInfo in cueSheetInfos) + { + if (cueSheetInfo.CueSheet != null) + { + decisions.ForEach(item => + { + if (cueSheetInfo.IsForMediaFile(item.Item.Path)) + { + item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path; + } + }); + } + + mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); + } + } + + decisions.AddRange(_importDecisionMaker.GetImportDecisions(mediaFileList, null, itemInfo, config)); + + decisionsStopwatch.Stop(); + _logger.Debug("Import decisions complete [{0}]", decisionsStopwatch.Elapsed); + + Import(folders, artistIds, decisions); + } + + private List GetMediaFiles(List folders, List artistIds) + { var mediaFileList = new List(); var musicFilesStopwatch = Stopwatch.StartNew(); @@ -96,7 +158,7 @@ namespace NzbDrone.Core.MediaFiles if (rootFolder == null) { _logger.Error("Not scanning {0}, it's not a subdirectory of a defined root folder", folder); - return; + return mediaFileList; } var folderExists = _diskProvider.FolderExists(folder); @@ -108,7 +170,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Warn("Artists' root folder ({0}) doesn't exist.", rootFolder.Path); var skippedArtists = _artistService.GetArtists(artistIds); skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist))); - return; + return mediaFileList; } if (_diskProvider.FolderEmpty(rootFolder.Path)) @@ -116,7 +178,7 @@ namespace NzbDrone.Core.MediaFiles _logger.Warn("Artists' root folder ({0}) is empty.", rootFolder.Path); var skippedArtists = _artistService.GetArtists(artistIds); skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderIsEmpty))); - return; + return mediaFileList; } } @@ -140,26 +202,16 @@ namespace NzbDrone.Core.MediaFiles CleanMediaFiles(folder, files.Select(x => x.FullName).ToList()); mediaFileList.AddRange(files); - mediaFileList.RemoveAll(x => x.Extension == ".cue"); } musicFilesStopwatch.Stop(); _logger.Trace("Finished getting track files for:\n{0} [{1}]", folders.ConcatToString("\n"), musicFilesStopwatch.Elapsed); - var decisionsStopwatch = Stopwatch.StartNew(); - - var config = new ImportDecisionMakerConfig - { - Filter = filter, - IncludeExisting = true, - AddNewArtists = addNewArtists - }; - - var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, null, null, config); - - decisionsStopwatch.Stop(); - _logger.Debug("Import decisions complete [{0}]", decisionsStopwatch.Elapsed); + return mediaFileList; + } + private void Import(List folders, List artistIds, List> decisions) + { var importStopwatch = Stopwatch.StartNew(); _importApprovedTracks.Import(decisions, false); @@ -178,7 +230,8 @@ namespace NzbDrone.Core.MediaFiles Modified = decision.Item.Modified, DateAdded = DateTime.UtcNow, Quality = decision.Item.Quality, - MediaInfo = decision.Item.FileTrackInfo.MediaInfo + MediaInfo = decision.Item.FileTrackInfo.MediaInfo, + IsSingleFileRelease = decision.Item.IsSingleFileRelease, }) .ToList(); _mediaFileService.AddMany(newFiles); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs index 63d57512f..12be78699 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs @@ -71,8 +71,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators { 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[i].FileTrackInfo.Year = (uint)tracks[i].Album.ReleaseDate.Value.Year; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index bf5b76501..537919552 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { public interface IIdentificationService { - List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config); + List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config, List cueSheetInfos = null); } public class IdentificationService : IIdentificationService @@ -114,7 +114,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return releases; } - public List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) + public List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config, List cueSheetInfos = null) { // 1 group localTracks so that we think they represent a single release // 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk. @@ -132,6 +132,41 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification i++; _logger.ProgressInfo($"Identifying album {i}/{releases.Count}"); IdentifyRelease(localRelease, idOverrides, config); + + if (cueSheetInfos != null && localRelease.IsSingleFileRelease) + { + var addedMbTracks = new List(); + localRelease.LocalTracks.ForEach(localTrack => + { + var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); + var cueSheet = cueSheetFindResult?.CueSheet; + if (cueSheet == null) + { + return; + } + + localTrack.Tracks.Clear(); + localRelease.AlbumRelease.Tracks.Value.ForEach(mbTrack => + { + cueSheet.Files[0].Tracks.ForEach(cueTrack => + { + if (!string.Equals(cueTrack.Title, mbTrack.Title, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (addedMbTracks.Contains(mbTrack)) + { + return; + } + + mbTrack.IsSingleFileRelease = true; + localTrack.Tracks.Add(mbTrack); + addedMbTracks.Add(mbTrack); + }); + }); + }); + } } watch.Stop(); @@ -187,7 +222,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification FileTrackInfo = _audioTagService.ReadTags(x.Path), ExistingFile = true, AdditionalFile = true, - Quality = x.Quality + Quality = x.Quality, + IsSingleFileRelease = x.IsSingleFileRelease, })) .ToList(); @@ -340,19 +376,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification localAlbumRelease.AlbumRelease = release; localAlbumRelease.ExistingTracks = extraTracks; localAlbumRelease.TrackMapping = mapping; - if (localAlbumRelease.IsSingleFileRelease) - { - 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) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index b1bbc4ec6..3236d82c1 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.IO.Abstractions; using System.Linq; using DryIoc.ImTools; @@ -11,6 +12,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.TrackImport.Aggregation; using NzbDrone.Core.MediaFiles.TrackImport.Identification; using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.RootFolders; @@ -19,7 +21,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { public interface IMakeImportDecision { - List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); + List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config, List cueSheetInfos = null); + List> GetImportDecisions(List cueSheetInfos, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); + CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles); } public class IdentificationOverrides @@ -29,12 +33,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public AlbumRelease AlbumRelease { get; set; } } + public class CueSheetInfo + { + public List MusicFiles { get; set; } + public IdentificationOverrides IdOverrides { get; set; } + public CueSheet CueSheet { get; set; } + public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); + } + public class ImportDecisionMakerInfo { public DownloadClientItem DownloadClientItem { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } - public bool IsSingleFileRelease { get; set; } - public CueSheet CueSheet { get; set; } } public class ImportDecisionMakerConfig @@ -51,6 +61,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport private readonly IEnumerable> _trackSpecifications; private readonly IEnumerable> _albumSpecifications; private readonly IMediaFileService _mediaFileService; + private readonly IParsingService _parsingService; private readonly IAudioTagService _audioTagService; private readonly IAugmentingService _augmentingService; private readonly IIdentificationService _identificationService; @@ -61,6 +72,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public ImportDecisionMaker(IEnumerable> trackSpecifications, IEnumerable> albumSpecifications, IMediaFileService mediaFileService, + IParsingService parsingService, IAudioTagService audioTagService, IAugmentingService augmentingService, IIdentificationService identificationService, @@ -71,6 +83,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _trackSpecifications = trackSpecifications; _albumSpecifications = albumSpecifications; _mediaFileService = mediaFileService; + _parsingService = parsingService; _audioTagService = audioTagService; _augmentingService = augmentingService; _identificationService = identificationService; @@ -79,6 +92,46 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _logger = logger; } + public CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles) + { + var cueSheetInfo = new CueSheetInfo(); + var cueSheet = new CueSheet(cueFile); + if (cueSheet == null) + { + return cueSheetInfo; + } + + cueSheetInfo.CueSheet = cueSheet; + cueSheetInfo.IdOverrides = new IdentificationOverrides(); + + Artist artistFromCue = null; + if (!cueSheet.Performer.Empty()) + { + artistFromCue = _parsingService.GetArtist(cueSheet.Performer); + if (artistFromCue != null) + { + cueSheetInfo.IdOverrides.Artist = artistFromCue; + } + } + + 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) + { + cueSheetInfo.IdOverrides.Album = albumsFromCue[0]; + } + + cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); + + return cueSheetInfo; + } + public Tuple, List>> GetLocalTracks(List musicFiles, DownloadClientItem downloadClientItem, ParsedAlbumInfo folderInfo, FilterFilesType filter) { var watch = new System.Diagnostics.Stopwatch(); @@ -116,7 +169,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport Size = file.Length, Modified = file.LastWriteTimeUtc, FileTrackInfo = _audioTagService.ReadTags(file.FullName), - AdditionalFile = false + AdditionalFile = false, }; try @@ -142,7 +195,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return Tuple.Create(localTracks, decisions); } - public List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) + public List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config, List cueSheetInfos) { idOverrides ??= new IdentificationOverrides(); itemInfo ??= new ImportDecisionMakerInfo(); @@ -152,14 +205,39 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var decisions = trackData.Item2; localTracks.ForEach(x => x.ExistingFile = !config.NewDownload); - localTracks.ForEach(x => x.IsSingleFileRelease = itemInfo.IsSingleFileRelease); - if (itemInfo.IsSingleFileRelease) + if (cueSheetInfos != null) { - localTracks.ForEach(x => x.Artist = idOverrides.Artist); - localTracks.ForEach(x => x.Album = idOverrides.Album); + localTracks.ForEach(localTrack => + { + var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); + var cueSheet = cueSheetFindResult?.CueSheet; + if (cueSheet != null) + { + localTrack.IsSingleFileRelease = cueSheet.IsSingleFileRelease; + localTrack.Artist = idOverrides.Artist; + localTrack.Album = idOverrides.Album; + } + }); } - var releases = _identificationService.Identify(localTracks, idOverrides, config); + var localTracksByAlbums = localTracks.GroupBy(x => x.Album); + foreach (var localTracksByAlbum in localTracksByAlbums) + { + if (!localTracksByAlbum.All(x => x.IsSingleFileRelease == true)) + { + continue; + } + + localTracks.ForEach(x => + { + if (x.IsSingleFileRelease && localTracksByAlbum.Contains(x)) + { + x.FileTrackInfo.DiscCount = localTracksByAlbum.Count(); + } + }); + } + + var releases = _identificationService.Identify(localTracks, idOverrides, config, cueSheetInfos); var albums = releases.GroupBy(x => x.AlbumRelease?.Album?.Value.ForeignAlbumId); @@ -206,6 +284,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return decisions; } + public List> GetImportDecisions(List cueSheetInfos, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) + { + var decisions = new List>(); + foreach (var cueSheetInfo in cueSheetInfos) + { + decisions.AddRange(GetImportDecisions(cueSheetInfo.MusicFiles, cueSheetInfo.IdOverrides, itemInfo, config, cueSheetInfos)); + } + + return decisions; + } + private void EnsureData(LocalAlbumRelease release) { if (release.AlbumRelease != null && release.AlbumRelease.Album.Value.Artist.Value.QualityProfileId == 0) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 3adf9cd4f..598ebf4b1 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.TrackedDownloads; @@ -155,63 +156,67 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual // Split cue and non-cue files var cueFiles = audioFiles.Where(x => x.Extension.Equals(".cue")).ToList(); audioFiles.RemoveAll(l => cueFiles.Contains(l)); + var cueSheetInfos = new List(); foreach (var cueFile in cueFiles) { - var cueSheet = new CueSheet(cueFile); - - Artist artistFromCue = null; - if (!cueSheet.Performer.Empty()) - { - artistFromCue = _parsingService.GetArtist(cueSheet.Performer); - } - - if (artistFromCue == null) - { - continue; - } + var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); + cueSheetInfos.Add(cueSheetInfo); + } - // TODO use the audio files from the cue sheet - var validAudioFiles = audioFiles.FindAll(x => cueSheet.FileNames.Contains(x.Name)); - if (validAudioFiles.Count == 0) + var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); + foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) + { + var audioFilesForCues = new List(); + foreach (var cueSheetInfo in cueSheetInfoGroup) { - continue; + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); } - 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 manualImportItems = ProcessFolder(downloadId, cueSheetInfos[0].IdOverrides, filter, replaceExistingFiles, downloadClientItem, cueSheetInfos[0].IdOverrides.Album.Title, audioFilesForCues, cueSheetInfos); + results.AddRange(manualImportItems); - results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, cueSheet.Title, validAudioFiles, cueSheet)); - audioFiles.RemoveAll(x => validAudioFiles.Contains(x)); + RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); } - results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, null)); + var idOverrides = new IdentificationOverrides + { + Artist = artist, + Album = null + }; + + results.AddRange(ProcessFolder(downloadId, idOverrides, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles)); return results; } - private List ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, CueSheet cueSheet) + private void RemoveProcessedAudioFiles(List audioFiles, List cueSheetInfos, List manualImportItems) { - var idOverrides = new IdentificationOverrides + foreach (var cueSheetInfo in cueSheetInfos) { - Artist = overrideArtist, - Album = overrideAlbum - }; + if (cueSheetInfo.CueSheet != null) + { + manualImportItems.ForEach(item => + { + if (cueSheetInfo.IsForMediaFile(item.Path)) + { + item.CueSheetPath = cueSheetInfo.CueSheet.Path; + } + }); + } + + audioFiles.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); + } + } + + private List ProcessFolder(string downloadId, IdentificationOverrides idOverrides, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, List cueSheetInfos = null) + { + idOverrides ??= new IdentificationOverrides(); var itemInfo = new ImportDecisionMakerInfo { DownloadClientItem = downloadClientItem, ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle), - CueSheet = cueSheet, - IsSingleFileRelease = cueSheet != null ? cueSheet.IsSingleFileRelease : false, }; + var config = new ImportDecisionMakerConfig { Filter = filter, @@ -221,7 +226,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AddNewArtists = false }; - var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config); + var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config, cueSheetInfos); // paths will be different for new and old files which is why we need to map separately var newFiles = audioFiles.Join(decisions, @@ -230,16 +235,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual (f, d) => new { File = f, Decision = d }, PathEqualityComparer.Instance); - var newItems = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false)); + var newItemsList = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false)).ToList(); + var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision)); var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false)); - var itemsList = newItems.Concat(existingItems).ToList(); - if (cueSheet != null) - { - itemsList.ForEach(item => { item.CueSheetPath = cueSheet.Path; }); - } - + var itemsList = newItemsList.Concat(existingItems.ToList()).ToList(); return itemsList; } @@ -257,13 +258,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var disableReleaseSwitching = group.First().DisableReleaseSwitching; - var files = group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList(); - var idOverride = new IdentificationOverrides - { - Artist = group.First().Artist, - Album = group.First().Album, - AlbumRelease = group.First().Release - }; var config = new ImportDecisionMakerConfig { Filter = FilterFilesType.None, @@ -273,61 +267,96 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AddNewArtists = false }; - var itemInfo = new ImportDecisionMakerInfo + var audioFiles = new List(); + foreach (var item in group) { - IsSingleFileRelease = group.All(x => x.IsSingleFileRelease == true) - }; + var file = _diskProvider.GetFileInfo(item.Path); + audioFiles.Add(file); + } - // TODO support with the cue sheet - var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, itemInfo, config); + var cueSheetInfos = new List(); + var audioFilesForCues = new List(); + var itemInfo = new ImportDecisionMakerInfo(); + foreach (var item in group) + { + if (item.IsSingleFileRelease) + { + var cueFile = _diskProvider.GetFileInfo(item.CueSheetPath); + var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); + cueSheetInfos.Add(cueSheetInfo); + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + } + } + + var singleFileReleaseDecisions = _importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfos[0].IdOverrides, itemInfo, config, cueSheetInfos); + var manualImportItems = UpdateItems(group, singleFileReleaseDecisions, replaceExistingFiles, disableReleaseSwitching); + result.AddRange(manualImportItems); - var existingItems = group.Join(decisions, - i => i.Path, - d => d.Item.Path, - (i, d) => new { Item = i, Decision = d }, - PathEqualityComparer.Instance); + RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); - foreach (var pair in existingItems) + var idOverride = new IdentificationOverrides { - var item = pair.Item; - var decision = pair.Decision; + Artist = group.First().Artist, + Album = group.First().Album, + AlbumRelease = group.First().Release + }; + var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverride, itemInfo, config); + result.AddRange(UpdateItems(group, decisions, replaceExistingFiles, disableReleaseSwitching)); + } - if (decision.Item.Artist != null) - { - item.Artist = decision.Item.Artist; - } + return result; + } - if (decision.Item.Album != null) - { - item.Album = decision.Item.Album; - item.Release = decision.Item.Release; - } + private List UpdateItems(IGrouping group, List> decisions, bool replaceExistingFiles, bool disableReleaseSwitching) + { + var result = new List(); - if (decision.Item.Tracks.Any()) - { - item.Tracks = decision.Item.Tracks; - } + var existingItems = group.Join(decisions, + i => i.Path, + d => d.Item.Path, + (i, d) => new { Item = i, Decision = d }, + PathEqualityComparer.Instance); - if (item.Quality?.Quality == Quality.Unknown) - { - item.Quality = decision.Item.Quality; - } + foreach (var pair in existingItems) + { + var item = pair.Item; + var decision = pair.Decision; - if (item.ReleaseGroup.IsNullOrWhiteSpace()) - { - item.ReleaseGroup = decision.Item.ReleaseGroup; - } + if (decision.Item.Artist != null) + { + item.Artist = decision.Item.Artist; + } - item.Rejections = decision.Rejections; - item.Size = decision.Item.Size; + if (decision.Item.Album != null) + { + item.Album = decision.Item.Album; + item.Release = decision.Item.Release; + } - result.Add(item); + if (decision.Item.Tracks.Any()) + { + item.Tracks = decision.Item.Tracks; } - var newDecisions = decisions.Except(existingItems.Select(x => x.Decision)); - result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching))); + if (item.Quality?.Quality == Quality.Unknown) + { + item.Quality = decision.Item.Quality; + } + + if (item.ReleaseGroup.IsNullOrWhiteSpace()) + { + item.ReleaseGroup = decision.Item.ReleaseGroup; + } + + item.Rejections = decision.Rejections; + item.Size = decision.Item.Size; + + result.Add(item); } + var newDecisions = decisions.Except(existingItems.Select(x => x.Decision)); + result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching))); + return result; } From efd9a63fadc6d9795e6b862205d7548c4c49342b Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Thu, 12 Oct 2023 00:49:29 +0200 Subject: [PATCH 07/20] Fix a crash caused by missing track files. (cherry picked from commit f784ad1d5d1a604e490c9c73298d8ebb81cadd24) --- .../TrackImport/Specifications/AlbumUpgradeSpecification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs index f6deffbc9..7d5f1ff96 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/AlbumUpgradeSpecification.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications _logger.Debug("Min quality of new files: {0}", newMinQuality); // get minimum quality of existing release - var existingQualities = currentRelease.Tracks.Value.Where(x => x.TrackFileId != 0).Select(x => x.TrackFile.Value.Quality); + var existingQualities = currentRelease.Tracks.Value.Where(x => x.TrackFileId != 0 && x.TrackFile.Value != null).Select(x => x.TrackFile.Value.Quality); if (existingQualities.Any()) { var existingMinQuality = existingQualities.OrderBy(x => x, qualityComparer).First(); From 1f9adce15d1a7b440d2c307675fc24beec0c92df Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Thu, 12 Oct 2023 18:23:52 +0200 Subject: [PATCH 08/20] Create a cue file for the move file operation in the same way as the copy file operation. Use the correct ID overrides for processing grouped cue sheet files. (cherry picked from commit 6f069389bf23bbb0fc220463795bd03d81da7cfe) --- .../MediaFiles/DiskScanService.cs | 2 +- .../MediaFiles/TrackFileMovingService.cs | 33 +++++++++++-------- .../TrackImport/ImportDecisionMaker.cs | 2 +- .../TrackImport/Manual/ManualImportService.cs | 19 +++++++---- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 145f40cd6..ec202e298 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -116,7 +116,7 @@ namespace NzbDrone.Core.MediaFiles audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); } - decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfos[0].IdOverrides, itemInfo, config, cueSheetInfos)); + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); foreach (var cueSheetInfo in cueSheetInfos) { diff --git a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs index 5872047ba..7e24955d1 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFileMovingService.cs @@ -80,6 +80,8 @@ namespace NzbDrone.Core.MediaFiles EnsureTrackFolder(trackFile, localTrack, filePath); + TryToCreateCueFile(localTrack, filePath); + _logger.Debug("Moving track file: {0} to {1}", trackFile.Path, filePath); return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move); @@ -91,29 +93,34 @@ namespace NzbDrone.Core.MediaFiles EnsureTrackFolder(trackFile, localTrack, filePath); + TryToCreateCueFile(localTrack, filePath); + + if (_configService.CopyUsingHardlinks) + { + _logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath); + return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.HardLinkOrCopy); + } + + _logger.Debug("Copying track file: {0} to {1}", trackFile.Path, filePath); + return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy); + } + + private void TryToCreateCueFile(LocalTrack localTrack, string trackFilePath) + { if (localTrack.IsSingleFileRelease && !localTrack.CueSheetPath.Empty()) { - var directory = Path.GetDirectoryName(filePath); - var fileName = Path.GetFileNameWithoutExtension(filePath); + var directory = Path.GetDirectoryName(trackFilePath); + var fileName = Path.GetFileNameWithoutExtension(trackFilePath); var cueSheetPath = Path.Combine(directory, fileName + ".cue"); - _diskTransferService.TransferFile(localTrack.CueSheetPath, cueSheetPath, TransferMode.Copy); + _diskTransferService.TransferFile(localTrack.CueSheetPath, cueSheetPath, TransferMode.Copy, true); var lines = new List(File.ReadAllLines(cueSheetPath)); var fileLineIndex = lines.FindIndex(line => line.Contains("FILE")); if (fileLineIndex != -1) { - lines[fileLineIndex] = "FILE \"" + Path.GetFileName(filePath) + "\" WAVE"; + lines[fileLineIndex] = "FILE \"" + Path.GetFileName(trackFilePath) + "\" WAVE"; File.WriteAllLines(cueSheetPath, lines); } } - - if (_configService.CopyUsingHardlinks) - { - _logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath); - return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.HardLinkOrCopy); - } - - _logger.Debug("Copying track file: {0} to {1}", trackFile.Path, filePath); - return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy); } private TrackFile TransferFile(TrackFile trackFile, Artist artist, List tracks, string destinationFilePath, TransferMode mode) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 3236d82c1..7cd06bd52 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -35,7 +35,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public class CueSheetInfo { - public List MusicFiles { get; set; } + public List MusicFiles { get; set; } = new List(); public IdentificationOverrides IdOverrides { get; set; } public CueSheet CueSheet { get; set; } public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 598ebf4b1..f7c1d1da3 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -166,13 +166,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) { - var audioFilesForCues = new List(); - foreach (var cueSheetInfo in cueSheetInfoGroup) - { - audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); - } - - var manualImportItems = ProcessFolder(downloadId, cueSheetInfos[0].IdOverrides, filter, replaceExistingFiles, downloadClientItem, cueSheetInfos[0].IdOverrides.Album.Title, audioFilesForCues, cueSheetInfos); + var manualImportItems = ProcessFolder(downloadId, filter, replaceExistingFiles, downloadClientItem, cueSheetInfoGroup.ToList()); results.AddRange(manualImportItems); RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); @@ -208,6 +202,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual } } + private List ProcessFolder(string downloadId, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, List cueSheetInfos) + { + var audioFilesForCues = new List(); + foreach (var cueSheetInfo in cueSheetInfos) + { + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + } + + return ProcessFolder(downloadId, cueSheetInfos[0].IdOverrides, filter, replaceExistingFiles, downloadClientItem, cueSheetInfos[0].CueSheet.Title, audioFilesForCues, cueSheetInfos); + } + private List ProcessFolder(string downloadId, IdentificationOverrides idOverrides, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, List cueSheetInfos = null) { idOverrides ??= new IdentificationOverrides(); From 14bf91360ae5450b7319d940fb3e62a8f3fc598a Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Thu, 12 Oct 2023 20:01:43 +0200 Subject: [PATCH 09/20] Only process cue-originated audio files if there is any. (cherry picked from commit 4afaf5f058c682d7f2e1d7a9b51588dcd588b873) --- .../MediaFiles/DiskScanService.cs | 51 ++++++++++--------- .../TrackImport/Manual/ManualImportService.cs | 39 ++++++++------ 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index ec202e298..4c71e5f49 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -99,39 +99,42 @@ namespace NzbDrone.Core.MediaFiles var decisions = new List>(); var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList(); - mediaFileList.RemoveAll(l => cueFiles.Contains(l)); - var cueSheetInfos = new List(); - foreach (var cueFile in cueFiles) + if (cueFiles.Count > 0) { - var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, mediaFileList); - cueSheetInfos.Add(cueSheetInfo); - } - - var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); - foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) - { - var audioFilesForCues = new List(); - foreach (var cueSheetInfo in cueSheetInfoGroup) + mediaFileList.RemoveAll(l => cueFiles.Contains(l)); + var cueSheetInfos = new List(); + foreach (var cueFile in cueFiles) { - audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, mediaFileList); + cueSheetInfos.Add(cueSheetInfo); } - decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); - - foreach (var cueSheetInfo in cueSheetInfos) + var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); + foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) { - if (cueSheetInfo.CueSheet != null) + var audioFilesForCues = new List(); + foreach (var cueSheetInfo in cueSheetInfoGroup) { - decisions.ForEach(item => + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + } + + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); + + foreach (var cueSheetInfo in cueSheetInfos) + { + if (cueSheetInfo.CueSheet != null) { - if (cueSheetInfo.IsForMediaFile(item.Item.Path)) + decisions.ForEach(item => { - item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path; - } - }); + if (cueSheetInfo.IsForMediaFile(item.Item.Path)) + { + item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path; + } + }); + } + + mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); } - - mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index f7c1d1da3..c0edc06ee 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -155,21 +155,24 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual // Split cue and non-cue files var cueFiles = audioFiles.Where(x => x.Extension.Equals(".cue")).ToList(); - audioFiles.RemoveAll(l => cueFiles.Contains(l)); - var cueSheetInfos = new List(); - foreach (var cueFile in cueFiles) + if (cueFiles.Count > 0) { - var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); - cueSheetInfos.Add(cueSheetInfo); - } + audioFiles.RemoveAll(l => cueFiles.Contains(l)); + var cueSheetInfos = new List(); + foreach (var cueFile in cueFiles) + { + var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); + cueSheetInfos.Add(cueSheetInfo); + } - var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); - foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) - { - var manualImportItems = ProcessFolder(downloadId, filter, replaceExistingFiles, downloadClientItem, cueSheetInfoGroup.ToList()); - results.AddRange(manualImportItems); + var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); + foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) + { + var manualImportItems = ProcessFolder(downloadId, filter, replaceExistingFiles, downloadClientItem, cueSheetInfoGroup.ToList()); + results.AddRange(manualImportItems); - RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); + RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); + } } var idOverrides = new IdentificationOverrides @@ -293,11 +296,15 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual } } - var singleFileReleaseDecisions = _importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfos[0].IdOverrides, itemInfo, config, cueSheetInfos); - var manualImportItems = UpdateItems(group, singleFileReleaseDecisions, replaceExistingFiles, disableReleaseSwitching); - result.AddRange(manualImportItems); + if (audioFilesForCues.Count > 0) + { + var idOverrides = cueSheetInfos.Count > 0 ? cueSheetInfos[0].IdOverrides : null; + var singleFileReleaseDecisions = _importDecisionMaker.GetImportDecisions(audioFilesForCues, idOverrides, itemInfo, config, cueSheetInfos); + var manualImportItems = UpdateItems(group, singleFileReleaseDecisions, replaceExistingFiles, disableReleaseSwitching); + result.AddRange(manualImportItems); - RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); + RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); + } var idOverride = new IdentificationOverrides { From 2d5b6a1defeb79571231920a238d094ecdbd43c5 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Fri, 13 Oct 2023 20:57:33 +0200 Subject: [PATCH 10/20] Centralize most of the cue sheet-related features to a service. (cherry picked from commit 5636735ae645280132d94e64d2e45f1a4b5f6323) --- src/NzbDrone.Core/MediaFiles/CueSheet.cs | 163 --------- .../MediaFiles/CueSheetService.cs | 332 ++++++++++++++++++ .../MediaFiles/DiskScanService.cs | 43 +-- .../Identification/IdentificationService.cs | 35 -- .../TrackImport/ImportDecisionMaker.cs | 62 ---- .../TrackImport/Manual/ManualImportService.cs | 97 ++--- 6 files changed, 358 insertions(+), 374 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/CueSheetService.cs diff --git a/src/NzbDrone.Core/MediaFiles/CueSheet.cs b/src/NzbDrone.Core/MediaFiles/CueSheet.cs index 84374635e..3f05ba080 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheet.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheet.cs @@ -1,35 +1,10 @@ -using System; using System.Collections.Generic; -using System.IO.Abstractions; -using System.Text; -using System.Text.RegularExpressions; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaFiles { public class CueSheet : ModelBase { - public CueSheet(IFileInfo fileInfo) - { - Path = fileInfo.FullName; - - 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); - ParseCueSheet(lines); - - // Single-file cue means it's an unsplit image, which should be specially treated in the pipeline - IsSingleFileRelease = Files.Count == 1; - } - } - } - public class IndexEntry { public int Key { get; set; } @@ -59,143 +34,5 @@ namespace NzbDrone.Core.MediaFiles public string DiscID { get; set; } public string Title { get; set; } public string Performer { get; set; } - private static string _FileKey = "FILE"; - private static string _TrackKey = "TRACK"; - private static string _IndexKey = "INDEX"; - private static string _GenreKey = "REM GENRE"; - private static string _DateKey = "REM DATE"; - private static string _DiscIdKey = "REM DISCID"; - private static string _PerformerKey = "PERFORMER"; - private static string _TitleKey = "TITLE"; - - private string ExtractValue(string line, string keyword) - { - var pattern = keyword + @"\s+(?:(?:\""(.*?)\"")|(.+))"; - var match = Regex.Match(line, pattern); - - if (match.Success) - { - var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; - return value; - } - - return ""; - } - - private void ParseCueSheet(string[] lines) - { - var i = 0; - try - { - while (true) - { - var line = lines[i]; - if (line.StartsWith(_FileKey)) - { - line = line.Trim(); - line = line.Substring(_FileKey.Length).Trim(); - var filename = line.Split('"')[1]; - var fileDetails = new FileEntry { Name = filename }; - - i++; - line = lines[i]; - while (line.StartsWith(" ")) - { - line = line.Trim(); - if (line.StartsWith(_TrackKey)) - { - line = line.Substring(_TrackKey.Length).Trim(); - } - - var trackDetails = new TrackEntry(); - var trackInfo = line.Split(' '); - if (trackInfo.Length > 0) - { - if (int.TryParse(trackInfo[0], out var number)) - { - trackDetails.Number = number; - } - } - - i++; - line = lines[i]; - while (line.StartsWith(" ")) - { - line = line.Trim(); - if (line.StartsWith(_IndexKey)) - { - line = line.Substring(_IndexKey.Length).Trim(); - var parts = line.Split(' '); - if (parts.Length > 1) - { - if (int.TryParse(parts[0], out var key)) - { - var value = parts[1].Trim('"'); - trackDetails.Indices.Add(new IndexEntry { Key = key, Time = value }); - } - } - - i++; - line = lines[i]; - } - else if (line.StartsWith(_TitleKey)) - { - trackDetails.Title = ExtractValue(line, _TitleKey); - i++; - line = lines[i]; - } - else if (line.StartsWith(_PerformerKey)) - { - trackDetails.Performer = ExtractValue(line, _PerformerKey); - i++; - line = lines[i]; - } - else - { - i++; - line = lines[i]; - } - } - - fileDetails.Tracks.Add(trackDetails); - } - - Files.Add(fileDetails); - } - else if (line.StartsWith(_GenreKey)) - { - Genre = ExtractValue(line, _GenreKey); - i++; - } - else if (line.StartsWith(_DateKey)) - { - Date = ExtractValue(line, _DateKey); - i++; - } - else if (line.StartsWith(_DiscIdKey)) - { - DiscID = ExtractValue(line, _DiscIdKey); - i++; - } - else if (line.StartsWith(_PerformerKey)) - { - Performer = ExtractValue(line, _PerformerKey); - i++; - } - else if (line.StartsWith(_TitleKey)) - { - Title = ExtractValue(line, _TitleKey); - i++; - } - else - { - i++; - } - } - } - catch (IndexOutOfRangeException) - { - } - } } } diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs new file mode 100644 index 000000000..2783c4435 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles.TrackImport; +using NzbDrone.Core.Music; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles +{ + public class CueSheetInfo + { + public List MusicFiles { get; set; } = new List(); + public IdentificationOverrides IdOverrides { get; set; } + public CueSheet CueSheet { get; set; } + public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); + } + + public interface ICueSheetService + { + List> GetImportDecisions(ref List mediaFileList, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); + } + + public class CueSheetService : ICueSheetService + { + private readonly IParsingService _parsingService; + private readonly IMakeImportDecision _importDecisionMaker; + + private static string _FileKey = "FILE"; + private static string _TrackKey = "TRACK"; + private static string _IndexKey = "INDEX"; + private static string _GenreKey = "REM GENRE"; + private static string _DateKey = "REM DATE"; + private static string _DiscIdKey = "REM DISCID"; + private static string _PerformerKey = "PERFORMER"; + private static string _TitleKey = "TITLE"; + + public CueSheetService(IParsingService parsingService, + IMakeImportDecision importDecisionMaker) + { + _parsingService = parsingService; + _importDecisionMaker = importDecisionMaker; + } + + public List> GetImportDecisions(ref List mediaFileList, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) + { + var decisions = new List>(); + var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList(); + if (cueFiles.Count == 0) + { + return decisions; + } + + mediaFileList.RemoveAll(l => cueFiles.Contains(l)); + var cueSheetInfos = new List(); + foreach (var cueFile in cueFiles) + { + var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList); + cueSheetInfos.Add(cueSheetInfo); + } + + var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); + foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) + { + var audioFilesForCues = new List(); + foreach (var cueSheetInfo in cueSheetInfoGroup) + { + audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + } + + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); + + foreach (var cueSheetInfo in cueSheetInfos) + { + if (cueSheetInfo.CueSheet != null) + { + decisions.ForEach(item => + { + if (cueSheetInfo.IsForMediaFile(item.Item.Path)) + { + item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path; + } + }); + } + + mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); + } + } + + var addedTracks = new List(); + decisions.ForEach(decision => + { + if (!decision.Item.IsSingleFileRelease) + { + return; + } + + var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(decision.Item.Path)); + var cueSheet = cueSheetFindResult?.CueSheet; + if (cueSheet == null) + { + return; + } + + if (cueSheet.Files.Count == 0) + { + return; + } + + var tracksFromCueSheet = cueSheet.Files.SelectMany(x => x.Tracks).ToList(); + if (tracksFromCueSheet.Count == 0) + { + return; + } + + var tracksFromRelease = decision.Item.Release.Tracks.Value; + if (tracksFromRelease.Count == 0) + { + return; + } + + decision.Item.Tracks = tracksFromRelease.Where(trackFromRelease => !addedTracks.Contains(trackFromRelease) && tracksFromCueSheet.Any(trackFromCueSheet => string.Equals(trackFromCueSheet.Title, trackFromRelease.Title, StringComparison.InvariantCultureIgnoreCase))).ToList(); + addedTracks.AddRange(decision.Item.Tracks); + }); + + return decisions; + } + + private CueSheet LoadCueSheet(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); + var cueSheet = ParseLines(lines); + + // Single-file cue means it's an unsplit image, which should be specially treated in the pipeline + cueSheet.IsSingleFileRelease = cueSheet.Files.Count == 1; + cueSheet.Path = fileInfo.FullName; + + return cueSheet; + } + } + + return new CueSheet(); + } + + private string ExtractValue(string line, string keyword) + { + var pattern = keyword + @"\s+(?:(?:\""(.*?)\"")|(.+))"; + var match = Regex.Match(line, pattern); + + if (match.Success) + { + var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value; + return value; + } + + return ""; + } + + private CueSheet ParseLines(string[] lines) + { + var cueSheet = new CueSheet(); + + var i = 0; + try + { + while (true) + { + var line = lines[i]; + if (line.StartsWith(_FileKey)) + { + line = line.Trim(); + line = line.Substring(_FileKey.Length).Trim(); + var filename = line.Split('"')[1]; + var fileDetails = new CueSheet.FileEntry { Name = filename }; + + i++; + line = lines[i]; + while (line.StartsWith(" ")) + { + line = line.Trim(); + if (line.StartsWith(_TrackKey)) + { + line = line.Substring(_TrackKey.Length).Trim(); + } + + var trackDetails = new CueSheet.TrackEntry(); + var trackInfo = line.Split(' '); + if (trackInfo.Length > 0) + { + if (int.TryParse(trackInfo[0], out var number)) + { + trackDetails.Number = number; + } + } + + i++; + line = lines[i]; + while (line.StartsWith(" ")) + { + line = line.Trim(); + if (line.StartsWith(_IndexKey)) + { + line = line.Substring(_IndexKey.Length).Trim(); + var parts = line.Split(' '); + if (parts.Length > 1) + { + if (int.TryParse(parts[0], out var key)) + { + var value = parts[1].Trim('"'); + trackDetails.Indices.Add(new CueSheet.IndexEntry { Key = key, Time = value }); + } + } + + i++; + line = lines[i]; + } + else if (line.StartsWith(_TitleKey)) + { + trackDetails.Title = ExtractValue(line, _TitleKey); + i++; + line = lines[i]; + } + else if (line.StartsWith(_PerformerKey)) + { + trackDetails.Performer = ExtractValue(line, _PerformerKey); + i++; + line = lines[i]; + } + else + { + i++; + line = lines[i]; + } + } + + fileDetails.Tracks.Add(trackDetails); + } + + cueSheet.Files.Add(fileDetails); + } + else if (line.StartsWith(_GenreKey)) + { + cueSheet.Genre = ExtractValue(line, _GenreKey); + i++; + } + else if (line.StartsWith(_DateKey)) + { + cueSheet.Date = ExtractValue(line, _DateKey); + i++; + } + else if (line.StartsWith(_DiscIdKey)) + { + cueSheet.DiscID = ExtractValue(line, _DiscIdKey); + i++; + } + else if (line.StartsWith(_PerformerKey)) + { + cueSheet.Performer = ExtractValue(line, _PerformerKey); + i++; + } + else if (line.StartsWith(_TitleKey)) + { + cueSheet.Title = ExtractValue(line, _TitleKey); + i++; + } + else + { + i++; + } + } + } + catch (IndexOutOfRangeException) + { + } + + return cueSheet; + } + + private CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles) + { + var cueSheetInfo = new CueSheetInfo(); + var cueSheet = LoadCueSheet(cueFile); + if (cueSheet == null) + { + return cueSheetInfo; + } + + cueSheetInfo.CueSheet = cueSheet; + cueSheetInfo.IdOverrides = new IdentificationOverrides(); + + Artist artistFromCue = null; + if (!cueSheet.Performer.Empty()) + { + artistFromCue = _parsingService.GetArtist(cueSheet.Performer); + if (artistFromCue != null) + { + cueSheetInfo.IdOverrides.Artist = artistFromCue; + } + } + + 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) + { + cueSheetInfo.IdOverrides.Album = albumsFromCue[0]; + } + + cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); + + return cueSheetInfo; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 4c71e5f49..3504b619b 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Core.MediaFiles private readonly IDiskProvider _diskProvider; private readonly IMediaFileService _mediaFileService; private readonly IMakeImportDecision _importDecisionMaker; + private readonly ICueSheetService _cueSheetService; private readonly IImportApprovedTracks _importApprovedTracks; private readonly IArtistService _artistService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; @@ -54,6 +55,7 @@ namespace NzbDrone.Core.MediaFiles IDiskProvider diskProvider, IMediaFileService mediaFileService, IMakeImportDecision importDecisionMaker, + ICueSheetService cueSheetService, IImportApprovedTracks importApprovedTracks, IArtistService artistService, IRootFolderService rootFolderService, @@ -65,6 +67,7 @@ namespace NzbDrone.Core.MediaFiles _diskProvider = diskProvider; _mediaFileService = mediaFileService; _importDecisionMaker = importDecisionMaker; + _cueSheetService = cueSheetService; _importApprovedTracks = importApprovedTracks; _artistService = artistService; _mediaFileTableCleanupService = mediaFileTableCleanupService; @@ -98,46 +101,8 @@ namespace NzbDrone.Core.MediaFiles }; var decisions = new List>(); - var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList(); - if (cueFiles.Count > 0) - { - mediaFileList.RemoveAll(l => cueFiles.Contains(l)); - var cueSheetInfos = new List(); - foreach (var cueFile in cueFiles) - { - var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, mediaFileList); - cueSheetInfos.Add(cueSheetInfo); - } - - var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); - foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) - { - var audioFilesForCues = new List(); - foreach (var cueSheetInfo in cueSheetInfoGroup) - { - audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); - } - - decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); - - foreach (var cueSheetInfo in cueSheetInfos) - { - if (cueSheetInfo.CueSheet != null) - { - decisions.ForEach(item => - { - if (cueSheetInfo.IsForMediaFile(item.Item.Path)) - { - item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path; - } - }); - } - - mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); - } - } - } + decisions.AddRange(_cueSheetService.GetImportDecisions(ref mediaFileList, itemInfo, config)); decisions.AddRange(_importDecisionMaker.GetImportDecisions(mediaFileList, null, itemInfo, config)); decisionsStopwatch.Stop(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index 537919552..b45159e2f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -132,41 +132,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification i++; _logger.ProgressInfo($"Identifying album {i}/{releases.Count}"); IdentifyRelease(localRelease, idOverrides, config); - - if (cueSheetInfos != null && localRelease.IsSingleFileRelease) - { - var addedMbTracks = new List(); - localRelease.LocalTracks.ForEach(localTrack => - { - var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); - var cueSheet = cueSheetFindResult?.CueSheet; - if (cueSheet == null) - { - return; - } - - localTrack.Tracks.Clear(); - localRelease.AlbumRelease.Tracks.Value.ForEach(mbTrack => - { - cueSheet.Files[0].Tracks.ForEach(cueTrack => - { - if (!string.Equals(cueTrack.Title, mbTrack.Title, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - if (addedMbTracks.Contains(mbTrack)) - { - return; - } - - mbTrack.IsSingleFileRelease = true; - localTrack.Tracks.Add(mbTrack); - addedMbTracks.Add(mbTrack); - }); - }); - }); - } } watch.Stop(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 7cd06bd52..2a395a4f3 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; using System.Linq; using DryIoc.ImTools; @@ -22,8 +21,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public interface IMakeImportDecision { List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config, List cueSheetInfos = null); - List> GetImportDecisions(List cueSheetInfos, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); - CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles); } public class IdentificationOverrides @@ -33,14 +30,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public AlbumRelease AlbumRelease { get; set; } } - public class CueSheetInfo - { - public List MusicFiles { get; set; } = new List(); - public IdentificationOverrides IdOverrides { get; set; } - public CueSheet CueSheet { get; set; } - public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); - } - public class ImportDecisionMakerInfo { public DownloadClientItem DownloadClientItem { get; set; } @@ -92,46 +81,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport _logger = logger; } - public CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles) - { - var cueSheetInfo = new CueSheetInfo(); - var cueSheet = new CueSheet(cueFile); - if (cueSheet == null) - { - return cueSheetInfo; - } - - cueSheetInfo.CueSheet = cueSheet; - cueSheetInfo.IdOverrides = new IdentificationOverrides(); - - Artist artistFromCue = null; - if (!cueSheet.Performer.Empty()) - { - artistFromCue = _parsingService.GetArtist(cueSheet.Performer); - if (artistFromCue != null) - { - cueSheetInfo.IdOverrides.Artist = artistFromCue; - } - } - - 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) - { - cueSheetInfo.IdOverrides.Album = albumsFromCue[0]; - } - - cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); - - return cueSheetInfo; - } - public Tuple, List>> GetLocalTracks(List musicFiles, DownloadClientItem downloadClientItem, ParsedAlbumInfo folderInfo, FilterFilesType filter) { var watch = new System.Diagnostics.Stopwatch(); @@ -284,17 +233,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return decisions; } - public List> GetImportDecisions(List cueSheetInfos, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) - { - var decisions = new List>(); - foreach (var cueSheetInfo in cueSheetInfos) - { - decisions.AddRange(GetImportDecisions(cueSheetInfo.MusicFiles, cueSheetInfo.IdOverrides, itemInfo, config, cueSheetInfos)); - } - - return decisions; - } - private void EnsureData(LocalAlbumRelease release) { if (release.AlbumRelease != null && release.AlbumRelease.Album.Value.Artist.Value.QualityProfileId == 0) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index c0edc06ee..c64102657 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -37,6 +37,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual private readonly IRootFolderService _rootFolderService; private readonly IDiskScanService _diskScanService; private readonly IMakeImportDecision _importDecisionMaker; + private readonly ICueSheetService _cueSheetService; private readonly ICustomFormatCalculationService _formatCalculator; private readonly IArtistService _artistService; private readonly IAlbumService _albumService; @@ -55,6 +56,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual IRootFolderService rootFolderService, IDiskScanService diskScanService, IMakeImportDecision importDecisionMaker, + ICueSheetService cueSheetService, ICustomFormatCalculationService formatCalculator, IArtistService artistService, IAlbumService albumService, @@ -73,6 +75,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual _rootFolderService = rootFolderService; _diskScanService = diskScanService; _importDecisionMaker = importDecisionMaker; + _cueSheetService = cueSheetService; _formatCalculator = formatCalculator; _artistService = artistService; _albumService = albumService; @@ -153,28 +156,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual var audioFiles = _diskScanService.GetAudioFiles(folder).ToList(); var results = new List(); - // Split cue and non-cue files - var cueFiles = audioFiles.Where(x => x.Extension.Equals(".cue")).ToList(); - if (cueFiles.Count > 0) - { - audioFiles.RemoveAll(l => cueFiles.Contains(l)); - var cueSheetInfos = new List(); - foreach (var cueFile in cueFiles) - { - var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); - cueSheetInfos.Add(cueSheetInfo); - } - - var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); - foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId) - { - var manualImportItems = ProcessFolder(downloadId, filter, replaceExistingFiles, downloadClientItem, cueSheetInfoGroup.ToList()); - results.AddRange(manualImportItems); - - RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); - } - } - var idOverrides = new IdentificationOverrides { Artist = artist, @@ -186,36 +167,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual return results; } - private void RemoveProcessedAudioFiles(List audioFiles, List cueSheetInfos, List manualImportItems) - { - foreach (var cueSheetInfo in cueSheetInfos) - { - if (cueSheetInfo.CueSheet != null) - { - manualImportItems.ForEach(item => - { - if (cueSheetInfo.IsForMediaFile(item.Path)) - { - item.CueSheetPath = cueSheetInfo.CueSheet.Path; - } - }); - } - - audioFiles.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x)); - } - } - - private List ProcessFolder(string downloadId, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, List cueSheetInfos) - { - var audioFilesForCues = new List(); - foreach (var cueSheetInfo in cueSheetInfos) - { - audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); - } - - return ProcessFolder(downloadId, cueSheetInfos[0].IdOverrides, filter, replaceExistingFiles, downloadClientItem, cueSheetInfos[0].CueSheet.Title, audioFilesForCues, cueSheetInfos); - } - private List ProcessFolder(string downloadId, IdentificationOverrides idOverrides, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List audioFiles, List cueSheetInfos = null) { idOverrides ??= new IdentificationOverrides(); @@ -234,7 +185,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AddNewArtists = false }; - var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config, cueSheetInfos); + var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, itemInfo, config); + if (audioFiles.Count > 0) + { + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config, cueSheetInfos)); + } // paths will be different for new and old files which is why we need to map separately var newFiles = audioFiles.Join(decisions, @@ -280,40 +235,32 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { var file = _diskProvider.GetFileInfo(item.Path); audioFiles.Add(file); - } - var cueSheetInfos = new List(); - var audioFilesForCues = new List(); - var itemInfo = new ImportDecisionMakerInfo(); - foreach (var item in group) - { - if (item.IsSingleFileRelease) + if (item.CueSheetPath != null) { var cueFile = _diskProvider.GetFileInfo(item.CueSheetPath); - var cueSheetInfo = _importDecisionMaker.GetCueSheetInfo(cueFile, audioFiles); - cueSheetInfos.Add(cueSheetInfo); - audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); + audioFiles.Add(cueFile); } } - if (audioFilesForCues.Count > 0) - { - var idOverrides = cueSheetInfos.Count > 0 ? cueSheetInfos[0].IdOverrides : null; - var singleFileReleaseDecisions = _importDecisionMaker.GetImportDecisions(audioFilesForCues, idOverrides, itemInfo, config, cueSheetInfos); - var manualImportItems = UpdateItems(group, singleFileReleaseDecisions, replaceExistingFiles, disableReleaseSwitching); - result.AddRange(manualImportItems); - - RemoveProcessedAudioFiles(audioFiles, cueSheetInfos, manualImportItems); - } - + var itemInfo = new ImportDecisionMakerInfo(); var idOverride = new IdentificationOverrides { Artist = group.First().Artist, Album = group.First().Album, AlbumRelease = group.First().Release }; - var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverride, itemInfo, config); - result.AddRange(UpdateItems(group, decisions, replaceExistingFiles, disableReleaseSwitching)); + + var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, itemInfo, config); + if (audioFiles.Count > 0) + { + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverride, itemInfo, config)); + } + + if (decisions.Count > 0) + { + result.AddRange(UpdateItems(group, decisions, replaceExistingFiles, disableReleaseSwitching)); + } } return result; From 0bb5a10f108940ef321a17935611fce248e9e4f3 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 15 Oct 2023 12:42:26 +0200 Subject: [PATCH 11/20] Move the cue sheet info list into ImportDecisionMakerInfo to eliminate the IMakeImportDecision API change. Fix a crash when trying to import an album while the artist is not added yet. (cherry picked from commit d439677e3a0bf5c7f9f92a5ca0b72ad89c6ee912) --- src/NzbDrone.Core/MediaFiles/CueSheetService.cs | 9 ++++++--- .../Identification/IdentificationService.cs | 4 ++-- .../MediaFiles/TrackImport/ImportDecisionMaker.cs | 11 ++++++----- .../TrackImport/Manual/ManualImportService.cs | 4 ++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index 2783c4435..98c048e35 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -73,7 +73,9 @@ namespace NzbDrone.Core.MediaFiles audioFilesForCues.AddRange(cueSheetInfo.MusicFiles); } - decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfo, config, cueSheetInfos)); + var itemInfoWithCueSheetInfos = itemInfo; + itemInfoWithCueSheetInfos.CueSheetInfos = cueSheetInfoGroup.ToList(); + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfoWithCueSheetInfos, config)); foreach (var cueSheetInfo in cueSheetInfos) { @@ -124,7 +126,8 @@ namespace NzbDrone.Core.MediaFiles return; } - decision.Item.Tracks = tracksFromRelease.Where(trackFromRelease => !addedTracks.Contains(trackFromRelease) && tracksFromCueSheet.Any(trackFromCueSheet => string.Equals(trackFromCueSheet.Title, trackFromRelease.Title, StringComparison.InvariantCultureIgnoreCase))).ToList(); + // TODO diacritics could cause false positives here + decision.Item.Tracks = tracksFromRelease.Where(trackFromRelease => !addedTracks.Contains(trackFromRelease) && tracksFromCueSheet.Any(trackFromCueSheet => string.Equals(trackFromCueSheet.Title, trackFromRelease.Title, StringComparison.OrdinalIgnoreCase))).ToList(); addedTracks.AddRange(decision.Item.Tracks); }); @@ -314,7 +317,7 @@ namespace NzbDrone.Core.MediaFiles var parsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = cueSheet.Title, - ArtistName = artistFromCue.Name, + ArtistName = artistFromCue?.Name, ReleaseDate = cueSheet.Date, }; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index b45159e2f..058648eb1 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification { public interface IIdentificationService { - List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config, List cueSheetInfos = null); + List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config); } public class IdentificationService : IIdentificationService @@ -114,7 +114,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return releases; } - public List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config, List cueSheetInfos = null) + public List Identify(List localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) { // 1 group localTracks so that we think they represent a single release // 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk. diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 2a395a4f3..07cf098c0 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { public interface IMakeImportDecision { - List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config, List cueSheetInfos = null); + List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); } public class IdentificationOverrides @@ -34,6 +34,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { public DownloadClientItem DownloadClientItem { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } + public List CueSheetInfos { get; set; } = new List(); } public class ImportDecisionMakerConfig @@ -144,7 +145,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return Tuple.Create(localTracks, decisions); } - public List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config, List cueSheetInfos) + public List> GetImportDecisions(List musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) { idOverrides ??= new IdentificationOverrides(); itemInfo ??= new ImportDecisionMakerInfo(); @@ -154,11 +155,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var decisions = trackData.Item2; localTracks.ForEach(x => x.ExistingFile = !config.NewDownload); - if (cueSheetInfos != null) + if (!itemInfo.CueSheetInfos.Empty()) { localTracks.ForEach(localTrack => { - var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); + var cueSheetFindResult = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); var cueSheet = cueSheetFindResult?.CueSheet; if (cueSheet != null) { @@ -186,7 +187,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport }); } - var releases = _identificationService.Identify(localTracks, idOverrides, config, cueSheetInfos); + var releases = _identificationService.Identify(localTracks, idOverrides, config); var albums = releases.GroupBy(x => x.AlbumRelease?.Album?.Value.ForeignAlbumId); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index c64102657..ad0f0f68c 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -186,9 +186,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual }; var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, itemInfo, config); - if (audioFiles.Count > 0) + if (!audioFiles.Empty()) { - decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config, cueSheetInfos)); + decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config)); } // paths will be different for new and old files which is why we need to map separately From 88abe759277c03920ca328caca95b7cb944679a8 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 5 Nov 2023 13:13:33 +0100 Subject: [PATCH 12/20] Add multi-performer support to the cue sheet parser. Fix an incorrect early-out in the cue sheet line parsing function. Fix a crash caused by the invalid artist and album on the local tracks. Add support to remove duplicated cue sheet files. (cherry picked from commit 336c62e6141d67c1f547806f09d4871d26342dca) --- src/NzbDrone.Core/MediaFiles/CueSheet.cs | 4 +- .../MediaFiles/CueSheetService.cs | 249 +++++++++++------- .../Aggregators/AggregateFilenameInfo.cs | 6 +- 3 files changed, 156 insertions(+), 103 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/CueSheet.cs b/src/NzbDrone.Core/MediaFiles/CueSheet.cs index 3f05ba080..7ff4ee458 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheet.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheet.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.MediaFiles { public int Number { get; set; } public string Title { get; set; } - public string Performer { get; set; } + public List Performers { get; set; } = new List(); public List Indices { get; set; } = new List(); } @@ -33,6 +33,6 @@ namespace NzbDrone.Core.MediaFiles public string Date { get; set; } public string DiscID { get; set; } public string Title { get; set; } - public string Performer { get; set; } + public List Performers { get; set; } = new List(); } } diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index 98c048e35..7517a7769 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -5,7 +5,6 @@ using System.IO.Abstractions; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Music; using NzbDrone.Core.Parser; @@ -61,7 +60,25 @@ namespace NzbDrone.Core.MediaFiles foreach (var cueFile in cueFiles) { var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList); - cueSheetInfos.Add(cueSheetInfo); + + var addedCueSheetInfo = cueSheetInfos.Find(existingCueSheetInfo => existingCueSheetInfo.CueSheet.DiscID == cueSheetInfo.CueSheet.DiscID); + if (addedCueSheetInfo == null) + { + cueSheetInfos.Add(cueSheetInfo); + } + + // If there are multiple cue sheet files for the same disc, then we try to keep the last one or the one with the exact same name as the media file, if there's any + else if (cueSheetInfo.CueSheet.IsSingleFileRelease && addedCueSheetInfo.CueSheet.Files.Count > 0) + { + var mediaFileName = Path.GetFileName(addedCueSheetInfo.CueSheet.Files[0].Name); + var cueSheetFileName = Path.GetFileName(cueFile.Name); + + if (mediaFileName != cueSheetFileName) + { + cueSheetInfos.Remove(addedCueSheetInfo); + cueSheetInfos.Add(cueSheetInfo); + } + } } var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList(); @@ -120,6 +137,11 @@ namespace NzbDrone.Core.MediaFiles return; } + if (decision.Item.Release == null) + { + return; + } + var tracksFromRelease = decision.Item.Release.Tracks.Value; if (tracksFromRelease.Count == 0) { @@ -172,124 +194,142 @@ namespace NzbDrone.Core.MediaFiles return ""; } + private List ExtractPerformers(string line) + { + var delimiters = new char[] { ',', ';' }; + var performers = ExtractValue(line, _PerformerKey); + return performers.Split(delimiters, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); + } + + private bool GetNewLine(ref int index, ref string newLine, string[] lines) + { + if (index < lines.Length) + { + newLine = lines[index]; + index++; + return true; + } + + return false; + } + private CueSheet ParseLines(string[] lines) { var cueSheet = new CueSheet(); var i = 0; - try + string line = null; + + while (GetNewLine(ref i, ref line, lines)) { - while (true) + if (line.StartsWith(_FileKey)) { - var line = lines[i]; - if (line.StartsWith(_FileKey)) + line = line.Trim(); + line = line.Substring(_FileKey.Length).Trim(); + var filename = line.Split('"')[1]; + var fileDetails = new CueSheet.FileEntry { Name = filename }; + + if (!GetNewLine(ref i, ref line, lines)) + { + break; + } + + while (line.StartsWith(" ")) { line = line.Trim(); - line = line.Substring(_FileKey.Length).Trim(); - var filename = line.Split('"')[1]; - var fileDetails = new CueSheet.FileEntry { Name = filename }; + if (line.StartsWith(_TrackKey)) + { + line = line.Substring(_TrackKey.Length).Trim(); + } - i++; - line = lines[i]; - while (line.StartsWith(" ")) + var trackDetails = new CueSheet.TrackEntry(); + var trackInfo = line.Split(' '); + if (trackInfo.Length > 0) { - line = line.Trim(); - if (line.StartsWith(_TrackKey)) + if (int.TryParse(trackInfo[0], out var number)) { - line = line.Substring(_TrackKey.Length).Trim(); + trackDetails.Number = number; } + } - var trackDetails = new CueSheet.TrackEntry(); - var trackInfo = line.Split(' '); - if (trackInfo.Length > 0) - { - if (int.TryParse(trackInfo[0], out var number)) - { - trackDetails.Number = number; - } - } + if (!GetNewLine(ref i, ref line, lines)) + { + break; + } - i++; - line = lines[i]; - while (line.StartsWith(" ")) + while (line.StartsWith(" ")) + { + line = line.Trim(); + if (line.StartsWith(_IndexKey)) { - line = line.Trim(); - if (line.StartsWith(_IndexKey)) + line = line.Substring(_IndexKey.Length).Trim(); + var parts = line.Split(' '); + if (parts.Length > 1) { - line = line.Substring(_IndexKey.Length).Trim(); - var parts = line.Split(' '); - if (parts.Length > 1) + if (int.TryParse(parts[0], out var key)) { - if (int.TryParse(parts[0], out var key)) - { - var value = parts[1].Trim('"'); - trackDetails.Indices.Add(new CueSheet.IndexEntry { Key = key, Time = value }); - } + var value = parts[1].Trim('"'); + trackDetails.Indices.Add(new CueSheet.IndexEntry { Key = key, Time = value }); } - - i++; - line = lines[i]; - } - else if (line.StartsWith(_TitleKey)) - { - trackDetails.Title = ExtractValue(line, _TitleKey); - i++; - line = lines[i]; - } - else if (line.StartsWith(_PerformerKey)) - { - trackDetails.Performer = ExtractValue(line, _PerformerKey); - i++; - line = lines[i]; - } - else - { - i++; - line = lines[i]; } } + else if (line.StartsWith(_TitleKey)) + { + trackDetails.Title = ExtractValue(line, _TitleKey); + } + else if (line.StartsWith(_PerformerKey)) + { + trackDetails.Performers = ExtractPerformers(line); + } - fileDetails.Tracks.Add(trackDetails); + if (!GetNewLine(ref i, ref line, lines)) + { + break; + } } - cueSheet.Files.Add(fileDetails); - } - else if (line.StartsWith(_GenreKey)) - { - cueSheet.Genre = ExtractValue(line, _GenreKey); - i++; - } - else if (line.StartsWith(_DateKey)) - { - cueSheet.Date = ExtractValue(line, _DateKey); - i++; - } - else if (line.StartsWith(_DiscIdKey)) - { - cueSheet.DiscID = ExtractValue(line, _DiscIdKey); - i++; - } - else if (line.StartsWith(_PerformerKey)) - { - cueSheet.Performer = ExtractValue(line, _PerformerKey); - i++; - } - else if (line.StartsWith(_TitleKey)) - { - cueSheet.Title = ExtractValue(line, _TitleKey); - i++; - } - else - { - i++; + fileDetails.Tracks.Add(trackDetails); } + + cueSheet.Files.Add(fileDetails); + } + else if (line.StartsWith(_GenreKey)) + { + cueSheet.Genre = ExtractValue(line, _GenreKey); + } + else if (line.StartsWith(_DateKey)) + { + cueSheet.Date = ExtractValue(line, _DateKey); + } + else if (line.StartsWith(_DiscIdKey)) + { + cueSheet.DiscID = ExtractValue(line, _DiscIdKey); + } + else if (line.StartsWith(_PerformerKey)) + { + cueSheet.Performers = ExtractPerformers(line); + } + else if (line.StartsWith(_TitleKey)) + { + cueSheet.Title = ExtractValue(line, _TitleKey); } } - catch (IndexOutOfRangeException) + + return cueSheet; + } + + private Artist GetArtist(List performers) + { + if (performers.Count == 1) + { + return _parsingService.GetArtist(performers[0]); + } + else if (performers.Count > 1) { + return _parsingService.GetArtist("Various Artist"); } - return cueSheet; + return null; } private CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles) @@ -302,22 +342,37 @@ namespace NzbDrone.Core.MediaFiles } cueSheetInfo.CueSheet = cueSheet; + cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); + cueSheetInfo.IdOverrides = new IdentificationOverrides(); - Artist artistFromCue = null; - if (!cueSheet.Performer.Empty()) + var artistFromCue = GetArtist(cueSheet.Performers); + + if (artistFromCue == null && cueSheet.Files.Count > 0) { - artistFromCue = _parsingService.GetArtist(cueSheet.Performer); - if (artistFromCue != null) + foreach (var fileEntry in cueSheet.Files) { - cueSheetInfo.IdOverrides.Artist = artistFromCue; + foreach (var track in fileEntry.Tracks) + { + artistFromCue = GetArtist(track.Performers); + if (artistFromCue != null) + { + break; + } + } } } + // The cue sheet file is too incomplete in this case + if (artistFromCue == null) + { + return cueSheetInfo; + } + var parsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = cueSheet.Title, - ArtistName = artistFromCue?.Name, + ArtistName = artistFromCue.Name, ReleaseDate = cueSheet.Date, }; @@ -327,8 +382,6 @@ namespace NzbDrone.Core.MediaFiles cueSheetInfo.IdOverrides.Album = albumsFromCue[0]; } - cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); - return cueSheetInfo; } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs index 12be78699..1b0c96a0a 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Aggregation/Aggregators/AggregateFilenameInfo.cs @@ -69,11 +69,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators { 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.ArtistTitle = tracks[i].Artist?.Name; + tracks[i].FileTrackInfo.AlbumTitle = tracks[i].Album?.Title; // 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; + tracks[i].FileTrackInfo.Year = (uint)(tracks[i].Album?.ReleaseDate?.Year ?? 0); } } else From 853fdf7ff280faf52d63f61d5b16ba52c0ef910c Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 5 Nov 2023 18:56:52 +0100 Subject: [PATCH 13/20] Add encoding detector support for the cue sheet parser. Use ID overrides when manually import items from a cue sheet. (cherry picked from commit 21a2314a19b8aed71f86885cb13d9583ca346023) --- src/NzbDrone.Core/Lidarr.Core.csproj | 1 + .../MediaFiles/CueSheetService.cs | 23 +++++++++++++++---- .../MediaFiles/DiskScanService.cs | 2 +- .../TrackImport/Manual/ManualImportService.cs | 4 ++-- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 436a2404f..42e82a293 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -29,6 +29,7 @@ + diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index 7517a7769..28a8f160b 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -3,12 +3,13 @@ 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.Core.MediaFiles.TrackImport; using NzbDrone.Core.Music; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using UtfUnknown; namespace NzbDrone.Core.MediaFiles { @@ -22,13 +23,14 @@ namespace NzbDrone.Core.MediaFiles public interface ICueSheetService { - List> GetImportDecisions(ref List mediaFileList, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); + List> GetImportDecisions(ref List mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config); } public class CueSheetService : ICueSheetService { private readonly IParsingService _parsingService; private readonly IMakeImportDecision _importDecisionMaker; + private readonly Logger _logger; private static string _FileKey = "FILE"; private static string _TrackKey = "TRACK"; @@ -40,13 +42,15 @@ namespace NzbDrone.Core.MediaFiles private static string _TitleKey = "TITLE"; public CueSheetService(IParsingService parsingService, - IMakeImportDecision importDecisionMaker) + IMakeImportDecision importDecisionMaker, + Logger logger) { _parsingService = parsingService; _importDecisionMaker = importDecisionMaker; + _logger = logger; } - public List> GetImportDecisions(ref List mediaFileList, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) + public List> GetImportDecisions(ref List mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) { var decisions = new List>(); var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList(); @@ -60,6 +64,10 @@ namespace NzbDrone.Core.MediaFiles foreach (var cueFile in cueFiles) { var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList); + if (idOverrides != null) + { + cueSheetInfo.IdOverrides = idOverrides; + } var addedCueSheetInfo = cueSheetInfos.Find(existingCueSheetInfo => existingCueSheetInfo.CueSheet.DiscID == cueSheetInfo.CueSheet.DiscID); if (addedCueSheetInfo == null) @@ -161,7 +169,10 @@ namespace NzbDrone.Core.MediaFiles using (var fs = fileInfo.OpenRead()) { var bytes = new byte[fileInfo.Length]; - var encoding = new UTF8Encoding(true); + var result = CharsetDetector.DetectFromFile(fileInfo.FullName); // or pass FileInfo + var encoding = result.Detected.Encoding; + _logger.Debug("Detected encoding {0} for {1}", encoding.WebName, fileInfo.FullName); + string content; while (fs.Read(bytes, 0, bytes.Length) > 0) { @@ -369,6 +380,8 @@ namespace NzbDrone.Core.MediaFiles return cueSheetInfo; } + cueSheetInfo.IdOverrides.Artist = artistFromCue; + var parsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = cueSheet.Title, diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 3504b619b..2a157d828 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -102,7 +102,7 @@ namespace NzbDrone.Core.MediaFiles var decisions = new List>(); - decisions.AddRange(_cueSheetService.GetImportDecisions(ref mediaFileList, itemInfo, config)); + decisions.AddRange(_cueSheetService.GetImportDecisions(ref mediaFileList, null, itemInfo, config)); decisions.AddRange(_importDecisionMaker.GetImportDecisions(mediaFileList, null, itemInfo, config)); decisionsStopwatch.Stop(); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index ad0f0f68c..37efd9a96 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -185,7 +185,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AddNewArtists = false }; - var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, itemInfo, config); + var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, null, itemInfo, config); if (!audioFiles.Empty()) { decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config)); @@ -251,7 +251,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AlbumRelease = group.First().Release }; - var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, itemInfo, config); + var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, idOverride, itemInfo, config); if (audioFiles.Count > 0) { decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverride, itemInfo, config)); From 26678e79fada418eab381452890b7feba87b7aff Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 19 Nov 2023 16:51:10 +0100 Subject: [PATCH 14/20] Implement the track distance calculation for cue sheet tracks. Implement the diacritics and punctuation marks sanitation feature for the cue sheet track mapping. # Conflicts: # src/NzbDrone.Core/Lidarr.Core.csproj (cherry picked from commit 27622cebe9433b27bb0e7dfaf3720e2bd284e513) --- src/NzbDrone.Core/Lidarr.Core.csproj | 1 + .../MediaFiles/CueSheetService.cs | 92 ++++++++++++++++++- .../Identification/DistanceCalculator.cs | 52 +++++++++-- .../Identification/IdentificationService.cs | 50 ++++++++-- .../TrackImport/ImportDecisionMaker.cs | 5 +- .../Parser/Model/LocalAlbumRelease.cs | 4 +- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 2 + 7 files changed, 181 insertions(+), 25 deletions(-) diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 42e82a293..2be5ce39d 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -4,6 +4,7 @@ + diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index 28a8f160b..5c55a53c9 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; +using System.Text; using System.Text.RegularExpressions; +using Diacritics.Extensions; using NLog; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Music; @@ -19,6 +21,15 @@ namespace NzbDrone.Core.MediaFiles public IdentificationOverrides IdOverrides { get; set; } public CueSheet CueSheet { get; set; } public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); + public CueSheet.FileEntry TryToGetFileEntryForMediaFile(string path) + { + if (CueSheet != null && CueSheet.Files.Count > 0) + { + return CueSheet.Files.Find(x => Path.GetFileName(path) == x.Name); + } + + return null; + } } public interface ICueSheetService @@ -50,6 +61,39 @@ namespace NzbDrone.Core.MediaFiles _logger = logger; } + private class PunctuationReplacer + { + private readonly Dictionary _replacements = new Dictionary + { + { '‘', '\'' }, { '’', '\'' }, // Single quotes + { '“', '"' }, { '”', '"' }, // Double quotes + { '‹', '<' }, { '›', '>' }, // Angle quotes + { '«', '<' }, { '»', '>' }, // Guillemets + { '–', '-' }, { '—', '-' }, // Dashes + { '…', '.' }, // Ellipsis + { '¡', '!' }, { '¿', '?' }, // Inverted punctuation (Spanish) + }; + + public string ReplacePunctuation(string input) + { + var output = new StringBuilder(input.Length); + + foreach (var c in input) + { + if (_replacements.TryGetValue(c, out var replacement)) + { + output.Append(replacement); + } + else + { + output.Append(c); + } + } + + return output.ToString(); + } + } + public List> GetImportDecisions(ref List mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config) { var decisions = new List>(); @@ -119,7 +163,6 @@ namespace NzbDrone.Core.MediaFiles } } - var addedTracks = new List(); decisions.ForEach(decision => { if (!decision.Item.IsSingleFileRelease) @@ -156,14 +199,53 @@ namespace NzbDrone.Core.MediaFiles return; } - // TODO diacritics could cause false positives here - decision.Item.Tracks = tracksFromRelease.Where(trackFromRelease => !addedTracks.Contains(trackFromRelease) && tracksFromCueSheet.Any(trackFromCueSheet => string.Equals(trackFromCueSheet.Title, trackFromRelease.Title, StringComparison.OrdinalIgnoreCase))).ToList(); - addedTracks.AddRange(decision.Item.Tracks); + var replacer = new PunctuationReplacer(); + var i = 0; + while (i < tracksFromRelease.Count) + { + var trackFromRelease = tracksFromRelease[i]; + var trackFromReleaseTitle = NormalizeTitle(replacer, trackFromRelease.Title); + + var j = 0; + var anyMatch = false; + while (j < tracksFromCueSheet.Count) + { + var trackFromCueSheet = tracksFromCueSheet[j]; + var trackFromCueSheetTitle = NormalizeTitle(replacer, trackFromCueSheet.Title); + anyMatch = string.Equals(trackFromReleaseTitle, trackFromCueSheetTitle, StringComparison.InvariantCultureIgnoreCase); + + if (anyMatch) + { + decision.Item.Tracks.Add(trackFromRelease); + tracksFromRelease.RemoveAt(i); + tracksFromCueSheet.RemoveAt(j); + + break; + } + else + { + j++; + } + } + + if (!anyMatch) + { + i++; + } + } }); return decisions; } + private static string NormalizeTitle(PunctuationReplacer replacer, string title) + { + title.Normalize(NormalizationForm.FormKD); + title = title.RemoveDiacritics(); + title = replacer.ReplacePunctuation(title); + return title; + } + private CueSheet LoadCueSheet(IFileInfo fileInfo) { using (var fs = fileInfo.OpenRead()) @@ -337,7 +419,7 @@ namespace NzbDrone.Core.MediaFiles } else if (performers.Count > 1) { - return _parsingService.GetArtist("Various Artist"); + return _parsingService.GetArtist("various artists"); } return null; diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs index 4b53efff4..d951a3938 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/DistanceCalculator.cs @@ -30,6 +30,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber; } + private static bool TrackIndexIncorrect(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber) + { + return cuesheetTrack.Number != mbTrack.AbsoluteTrackNumber; + } + public static int GetTotalTrackNumber(Track track, List allTracks) { return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber); @@ -79,6 +84,28 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return dist; } + public static Distance TrackDistance(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false) + { + var dist = new Distance(); + + // musicbrainz never has 'featuring' in the track title + // see https://musicbrainz.org/doc/Style/Artist_Credits + dist.AddString("track_title", cuesheetTrack.Title ?? "", mbTrack.Title); + + if (includeArtist && cuesheetTrack.Performers.Count == 1 + && !VariousArtistNames.Any(x => x.Equals(cuesheetTrack.Performers[0], StringComparison.InvariantCultureIgnoreCase))) + { + dist.AddString("track_artist", cuesheetTrack.Performers[0], mbTrack.ArtistMetadata.Value.Name); + } + + if (mbTrack.AbsoluteTrackNumber > 0) + { + dist.AddBool("track_index", TrackIndexIncorrect(cuesheetTrack, mbTrack, totalTrackNumber)); + } + + return dist; + } + public static Distance AlbumReleaseDistance(List localTracks, AlbumRelease release, TrackMapping mapping) { var dist = new Distance(); @@ -179,9 +206,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification } // tracks - if (localTracks.All(x => x.IsSingleFileRelease == true)) + if (mapping.CuesheetTrackMapping.Count != 0) { - dist.Add("tracks", 0); + foreach (var pair in mapping.CuesheetTrackMapping) + { + dist.Add("tracks", pair.Value.Item2.NormalizedDistance()); + } + + Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance()); } else { @@ -192,14 +224,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance()); - // missing tracks - foreach (var track in mapping.MBExtra.Take(localTracks.Count)) - { - dist.Add("missing_tracks", 1.0); - } - - Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance()); - // unmatched tracks foreach (var track in mapping.LocalExtra.Take(localTracks.Count)) { @@ -209,6 +233,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance()); } + // missing tracks + foreach (var track in mapping.MBExtra.Take(localTracks.Count)) + { + dist.Add("missing_tracks", 1.0); + } + + Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance()); + return dist; } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs index 058648eb1..b1b7dff9f 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Identification/IdentificationService.cs @@ -323,7 +323,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList(); var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList(); - var mapping = MapReleaseTracks(allLocalTracks, release.Tracks.Value); + var isSingleFileRelease = allLocalTracks.All(x => x.IsSingleFileRelease == true); + var mapping = isSingleFileRelease ? MapSingleFileReleaseTracks(allLocalTracks, release.Tracks.Value) : MapReleaseTracks(allLocalTracks, release.Tracks.Value); var distance = DistanceCalculator.AlbumReleaseDistance(allLocalTracks, release, mapping); var currDistance = distance.NormalizedDistance(); @@ -356,12 +357,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification public TrackMapping MapReleaseTracks(List localTracks, List mbTracks) { var result = new TrackMapping(); - result.IsSingleFileRelease = localTracks.All(x => x.IsSingleFileRelease == true); - if (result.IsSingleFileRelease) - { - return result; - } - var distances = new Distance[localTracks.Count, mbTracks.Count]; var costs = new double[localTracks.Count, mbTracks.Count]; @@ -392,5 +387,46 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification return result; } + + public TrackMapping MapSingleFileReleaseTracks(List localTracks, List mbTracks) + { + var result = new TrackMapping(); + + var cuesheetTracks = new List(); + foreach (var localTrack in localTracks) + { + if (localTrack.CueSheetFileEntry != null) + { + cuesheetTracks.AddRange(localTrack.CueSheetFileEntry.Tracks); + } + } + + var distances = new Distance[cuesheetTracks.Count, mbTracks.Count]; + var costs = new double[cuesheetTracks.Count, mbTracks.Count]; + + for (var col = 0; col < mbTracks.Count; col++) + { + var totalTrackNumber = DistanceCalculator.GetTotalTrackNumber(mbTracks[col], mbTracks); + for (var row = 0; row < cuesheetTracks.Count; row++) + { + distances[row, col] = DistanceCalculator.TrackDistance(cuesheetTracks[row], mbTracks[col], totalTrackNumber, false); + costs[row, col] = distances[row, col].NormalizedDistance(); + } + } + + var m = new Munkres(costs); + m.Run(); + + foreach (var pair in m.Solution) + { + result.CuesheetTrackMapping.Add(cuesheetTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2])); + _logger.Trace("Mapped {0} to {1}, dist: {2}", cuesheetTracks[pair.Item1], mbTracks[pair.Item2], costs[pair.Item1, pair.Item2]); + } + + result.MBExtra = mbTracks.Except(result.CuesheetTrackMapping.Values.Select(x => x.Item1)).ToList(); + _logger.Trace($"Missing tracks:\n{string.Join("\n", result.MBExtra)}"); + + return result; + } } } diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 07cf098c0..df7b1a19c 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -159,11 +159,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport { localTracks.ForEach(localTrack => { - var cueSheetFindResult = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); - var cueSheet = cueSheetFindResult?.CueSheet; + var cueSheetInfo = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path)); + var cueSheet = cueSheetInfo?.CueSheet; if (cueSheet != null) { localTrack.IsSingleFileRelease = cueSheet.IsSingleFileRelease; + localTrack.CueSheetFileEntry = cueSheetInfo.TryToGetFileEntryForMediaFile(localTrack.Path); localTrack.Artist = idOverrides.Artist; localTrack.Album = idOverrides.Album; } diff --git a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs index 0d1dd69c6..2ed3aa773 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalAlbumRelease.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Identification; using NzbDrone.Core.Music; @@ -70,11 +71,12 @@ namespace NzbDrone.Core.Parser.Model public TrackMapping() { Mapping = new Dictionary>(); + CuesheetTrackMapping = new Dictionary>(); } public Dictionary> Mapping { get; set; } public List LocalExtra { get; set; } public List MBExtra { get; set; } - public bool IsSingleFileRelease { get; set; } + public Dictionary> CuesheetTrackMapping { get; set; } } } diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 422ac2b87..9796fe0fb 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Identification; using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; @@ -32,6 +33,7 @@ namespace NzbDrone.Core.Parser.Model public string ReleaseGroup { get; set; } public string SceneName { get; set; } public bool IsSingleFileRelease { get; set; } + public CueSheet.FileEntry CueSheetFileEntry { get; set; } public string CueSheetPath { get; set; } public override string ToString() { From a7ab35fa146350df49d7fe7e53a25c10dc1766e0 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sat, 6 Jan 2024 13:41:00 +0100 Subject: [PATCH 15/20] Bump the database version for cue support to 076. (cherry picked from commit 32382ac052f60d60f6970c1fbf1d39a92f1dbd2b) (cherry picked from commit f3a5c7cfe3c593712c7b804f1342f28729b808fd) --- .../Migration/{073_add_flac_cue.cs => 076_add_flac_cue.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/NzbDrone.Core/Datastore/Migration/{073_add_flac_cue.cs => 076_add_flac_cue.cs} (95%) diff --git a/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs b/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs similarity index 95% rename from src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs rename to src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs index 0f8b8f5d6..08bb83a86 100644 --- a/src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs +++ b/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration { - [Migration(073)] + [Migration(076)] public class add_flac_cue : NzbDroneMigrationBase { protected override void MainDbUpgrade() From a581e64cd3c423c6630da402fca8c29f38e2c041 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sat, 6 Jan 2024 17:14:22 +0100 Subject: [PATCH 16/20] Only detect the .cue file encoding for the manual import service. (cherry picked from commit a3ccf87b01f071e2bf9db89e4248c730226d059c) (cherry picked from commit e1ab63906ac4761e0ae2ebfe040854f335f46476) --- .../MediaFiles/CueSheetService.cs | 27 ++++++++++++------- .../MediaFiles/DiskScanService.cs | 1 + .../TrackImport/ImportDecisionMaker.cs | 1 + .../TrackImport/Manual/ManualImportService.cs | 2 ++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index 5c55a53c9..efc54a771 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -107,7 +107,7 @@ namespace NzbDrone.Core.MediaFiles var cueSheetInfos = new List(); foreach (var cueFile in cueFiles) { - var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList); + var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList, itemInfo.DetectCueFileEncoding); if (idOverrides != null) { cueSheetInfo.IdOverrides = idOverrides; @@ -246,19 +246,26 @@ namespace NzbDrone.Core.MediaFiles return title; } - private CueSheet LoadCueSheet(IFileInfo fileInfo) + private CueSheet LoadCueSheet(IFileInfo fileInfo, bool detectCueFileEncoding) { using (var fs = fileInfo.OpenRead()) { var bytes = new byte[fileInfo.Length]; - var result = CharsetDetector.DetectFromFile(fileInfo.FullName); // or pass FileInfo - var encoding = result.Detected.Encoding; - _logger.Debug("Detected encoding {0} for {1}", encoding.WebName, fileInfo.FullName); - - string content; while (fs.Read(bytes, 0, bytes.Length) > 0) { - content = encoding.GetString(bytes); + string content; + if (detectCueFileEncoding) + { + var result = CharsetDetector.DetectFromFile(fileInfo.FullName); // or pass FileInfo + var encoding = result.Detected.Encoding; + _logger.Debug("Detected encoding {0} for {1}", encoding.WebName, fileInfo.FullName); + content = encoding.GetString(bytes); + } + else + { + content = Encoding.UTF8.GetString(bytes); + } + var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); var cueSheet = ParseLines(lines); @@ -425,10 +432,10 @@ namespace NzbDrone.Core.MediaFiles return null; } - private CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles) + private CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List musicFiles, bool detectCueFileEncoding) { var cueSheetInfo = new CueSheetInfo(); - var cueSheet = LoadCueSheet(cueFile); + var cueSheet = LoadCueSheet(cueFile, detectCueFileEncoding); if (cueSheet == null) { return cueSheetInfo; diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 2a157d828..ecdcfb41e 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -102,6 +102,7 @@ namespace NzbDrone.Core.MediaFiles var decisions = new List>(); + itemInfo.DetectCueFileEncoding = false; decisions.AddRange(_cueSheetService.GetImportDecisions(ref mediaFileList, null, itemInfo, config)); decisions.AddRange(_importDecisionMaker.GetImportDecisions(mediaFileList, null, itemInfo, config)); diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index df7b1a19c..9c5fb332e 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport public DownloadClientItem DownloadClientItem { get; set; } public ParsedAlbumInfo ParsedAlbumInfo { get; set; } public List CueSheetInfos { get; set; } = new List(); + public bool DetectCueFileEncoding { get; set; } } public class ImportDecisionMakerConfig diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs index 37efd9a96..ce7de7519 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportService.cs @@ -174,6 +174,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual { DownloadClientItem = downloadClientItem, ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle), + DetectCueFileEncoding = true, }; var config = new ImportDecisionMakerConfig @@ -251,6 +252,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual AlbumRelease = group.First().Release }; + itemInfo.DetectCueFileEncoding = true; var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, idOverride, itemInfo, config); if (audioFiles.Count > 0) { From 6140d5a3f4e239982ef5854b708105b09c103dd4 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 7 Jan 2024 16:07:35 +0100 Subject: [PATCH 17/20] Do not display "Is Single File Release" flag. (cherry picked from commit 80100df54ebf09d4708f6a5f541f9f8da34ecf48) --- .../Interactive/InteractiveImportModalContent.js | 5 ----- .../Interactive/InteractiveImportRow.js | 13 +------------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index f57dcbe3a..86fdcc0d1 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -53,11 +53,6 @@ const columns = [ label: () => translate('Tracks'), isVisible: true }, - { - name: 'isSingleFileRelease', - label: () => 'Is Single File Release', - isVisible: true - }, { name: 'cueSheetPath', label: () => 'Cue Sheet Path', diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index a2c05de62..14313c768 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -64,7 +64,6 @@ class InteractiveImportRow extends Component { artist, album, tracks, - isSingleFileRelease, quality, isSelected, onValidRowChange @@ -83,7 +82,7 @@ class InteractiveImportRow extends Component { const isValid = !!( artist && album && - (isSingleFileRelease || tracks.length) && + tracks.length && quality ); @@ -168,7 +167,6 @@ class InteractiveImportRow extends Component { album, albumReleaseId, tracks, - isSingleFileRelease, cueSheetPath, quality, releaseGroup, @@ -273,15 +271,6 @@ class InteractiveImportRow extends Component { - - { - isSingleFileRelease ? 'Yes' : 'No' - } - - Date: Sun, 7 Jan 2024 16:08:29 +0100 Subject: [PATCH 18/20] Remove "Is Single File Release" property from the track resource class and the database table. (cherry picked from commit b18cd458e032184e23874d1d23e3e26b24040b11) --- src/Lidarr.Api.V1/Tracks/TrackResource.cs | 2 -- src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Lidarr.Api.V1/Tracks/TrackResource.cs b/src/Lidarr.Api.V1/Tracks/TrackResource.cs index 59834b273..47f58811d 100644 --- a/src/Lidarr.Api.V1/Tracks/TrackResource.cs +++ b/src/Lidarr.Api.V1/Tracks/TrackResource.cs @@ -26,7 +26,6 @@ namespace Lidarr.Api.V1.Tracks public ArtistResource Artist { get; set; } public Ratings Ratings { get; set; } - public bool IsSingleFileRelease { get; set; } // Hiding this so people don't think its usable (only used to set the initial state) [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] @@ -59,7 +58,6 @@ namespace Lidarr.Api.V1.Tracks MediumNumber = model.MediumNumber, HasFile = model.HasFile, Ratings = model.Ratings, - IsSingleFileRelease = model.IsSingleFileRelease }; } diff --git a/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs b/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs index 08bb83a86..3e33d7132 100644 --- a/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs +++ b/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs @@ -8,7 +8,6 @@ namespace NzbDrone.Core.Datastore.Migration { protected override void MainDbUpgrade() { - Alter.Table("Tracks").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false); Alter.Table("TrackFiles").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false); } } From cd248eda71bab8a4a6308b9e56ae30cd303c8213 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Sun, 7 Jan 2024 16:11:14 +0100 Subject: [PATCH 19/20] Check against the .cue file directory when importing the media files to only import the files in the same directory. (cherry picked from commit 07c9213415f485ed9edf96f3cf3b5d84e095268f) --- src/NzbDrone.Core/MediaFiles/CueSheetService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs index efc54a771..0efd07df4 100644 --- a/src/NzbDrone.Core/MediaFiles/CueSheetService.cs +++ b/src/NzbDrone.Core/MediaFiles/CueSheetService.cs @@ -20,7 +20,7 @@ namespace NzbDrone.Core.MediaFiles public List MusicFiles { get; set; } = new List(); public IdentificationOverrides IdOverrides { get; set; } public CueSheet CueSheet { get; set; } - public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); + public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && (Path.GetDirectoryName(path) == Path.GetDirectoryName(CueSheet.Path)) && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name); public CueSheet.FileEntry TryToGetFileEntryForMediaFile(string path) { if (CueSheet != null && CueSheet.Files.Count > 0) @@ -442,7 +442,8 @@ namespace NzbDrone.Core.MediaFiles } cueSheetInfo.CueSheet = cueSheet; - cueSheetInfo.MusicFiles = musicFiles.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); + var musicFilesInTheSameDir = musicFiles.Where(musicFile => musicFile.DirectoryName == Path.GetDirectoryName(cueSheetInfo.CueSheet.Path)).ToList(); + cueSheetInfo.MusicFiles = musicFilesInTheSameDir.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList(); cueSheetInfo.IdOverrides = new IdentificationOverrides(); From 29e36e45ba6dfeaf2d4b9fa0fafc7a87c3b76061 Mon Sep 17 00:00:00 2001 From: zhangdoa Date: Wed, 21 Feb 2024 01:02:36 +0100 Subject: [PATCH 20/20] Bump the database version for cue support to 078. --- .../Migration/{076_add_flac_cue.cs => 078_add_flac_cue.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/NzbDrone.Core/Datastore/Migration/{076_add_flac_cue.cs => 078_add_flac_cue.cs} (94%) diff --git a/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs b/src/NzbDrone.Core/Datastore/Migration/078_add_flac_cue.cs similarity index 94% rename from src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs rename to src/NzbDrone.Core/Datastore/Migration/078_add_flac_cue.cs index 3e33d7132..c4cd96d50 100644 --- a/src/NzbDrone.Core/Datastore/Migration/076_add_flac_cue.cs +++ b/src/NzbDrone.Core/Datastore/Migration/078_add_flac_cue.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Datastore.Migration.Framework; namespace NzbDrone.Core.Datastore.Migration { - [Migration(076)] + [Migration(078)] public class add_flac_cue : NzbDroneMigrationBase { protected override void MainDbUpgrade()