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)
pull/4200/head
zhangdoa 7 months ago
parent 16a3fbe25b
commit c87efacb6a

@ -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 {
</TableRowCellButton>
<TableRowCellButton
isDisabled={!artist || !album || isSingleFileRelease}
isDisabled={!artist || !album}
title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined}
onPress={this.onSelectTrackPress}
>
@ -269,7 +268,7 @@ class InteractiveImportRow extends Component {
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
}
{
!isSingleFileRelease && showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
}
</TableRowCellButton>

@ -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<string> ReadFieldFromCuesheet(string[] lines, string fieldName)
{
var results = new List<string>();
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 "";
}
}
}

@ -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
{

@ -131,11 +131,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
private List<CandidateAlbumRelease> 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

@ -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);
}

@ -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<LocalTrack> localTracks, List<Track> 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;
}

@ -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<string> ReadFieldFromCuesheet(string[] lines, string fieldName)
{
var results = new List<string>();
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<ManualImportItem> 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<IFileInfo>
{
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<IFileInfo> { 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,

@ -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();

@ -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();
}

@ -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();
}

@ -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<string>();
@ -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);

@ -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

Loading…
Cancel
Save