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)
This commit is contained in:
zhangdoa 2023-10-04 19:27:49 +02:00
commit c87efacb6a
12 changed files with 154 additions and 120 deletions

View file

@ -65,7 +65,6 @@ class InteractiveImportRow extends Component {
album, album,
tracks, tracks,
isSingleFileRelease, isSingleFileRelease,
cuesheetPath,
quality, quality,
isSelected, isSelected,
onValidRowChange onValidRowChange
@ -84,7 +83,7 @@ class InteractiveImportRow extends Component {
const isValid = !!( const isValid = !!(
artist && artist &&
album && album &&
((isSingleFileRelease && cuesheetPath) || tracks.length) && (isSingleFileRelease || tracks.length) &&
quality quality
); );
@ -261,7 +260,7 @@ class InteractiveImportRow extends Component {
</TableRowCellButton> </TableRowCellButton>
<TableRowCellButton <TableRowCellButton
isDisabled={!artist || !album || isSingleFileRelease} isDisabled={!artist || !album}
title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined} title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined}
onPress={this.onSelectTrackPress} onPress={this.onSelectTrackPress}
> >
@ -269,7 +268,7 @@ class InteractiveImportRow extends Component {
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} /> showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
} }
{ {
!isSingleFileRelease && showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
} }
</TableRowCellButton> </TableRowCellButton>

View file

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

View file

@ -65,13 +65,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators
|| tracks.Any(x => x.FileTrackInfo.DiscNumber == 0)) || tracks.Any(x => x.FileTrackInfo.DiscNumber == 0))
{ {
_logger.Debug("Missing data in tags, trying filename augmentation"); _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; for (var i = 0; i < tracks.Count; ++i)
tracks[0].FileTrackInfo.AlbumTitle = tracks[0].Album.Title; {
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 // 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; tracks[i].FileTrackInfo.Year = (uint)tracks[i].Album.ReleaseDate.Value.Year;
}
} }
else else
{ {

View file

@ -131,7 +131,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
private List<CandidateAlbumRelease> GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting) 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) return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
.OrderBy(x => x.ReleaseDate) .OrderBy(x => x.ReleaseDate)

View file

@ -120,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
var releaseYear = release.ReleaseDate?.Year ?? 0; 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 // 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) if (isSameWithAlbumYear || localYear == releaseYear)
{ {
dist.Add("year", 0.0); dist.Add("year", 0.0);
@ -179,7 +179,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
} }
// tracks // tracks
if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease) if (localTracks.All(x => x.IsSingleFileRelease == true))
{ {
dist.Add("tracks", 0); dist.Add("tracks", 0);
} }

View file

@ -154,7 +154,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease) private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
{ {
if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) if (localAlbumRelease.IsSingleFileRelease)
{ {
return false; return false;
} }
@ -340,10 +340,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
localAlbumRelease.AlbumRelease = release; localAlbumRelease.AlbumRelease = release;
localAlbumRelease.ExistingTracks = extraTracks; localAlbumRelease.ExistingTracks = extraTracks;
localAlbumRelease.TrackMapping = mapping; localAlbumRelease.TrackMapping = mapping;
if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease) if (localAlbumRelease.IsSingleFileRelease)
{ {
localAlbumRelease.LocalTracks[0].Tracks = release.Tracks; localAlbumRelease.LocalTracks.ForEach(x => x.Tracks.Clear());
localAlbumRelease.LocalTracks[0].Tracks.ForEach(x => x.IsSingleFileRelease = true); 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) if (currDistance == 0.0)
@ -360,10 +368,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
public TrackMapping MapReleaseTracks(List<LocalTrack> localTracks, List<Track> mbTracks) public TrackMapping MapReleaseTracks(List<LocalTrack> localTracks, List<Track> mbTracks)
{ {
var result = new TrackMapping(); 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; return result;
} }

View file

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq; using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Crypto; using NzbDrone.Common.Crypto;
@ -134,33 +132,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
return ProcessFolder(path, downloadId, artist, filter, replaceExistingFiles); 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) private List<ManualImportItem> ProcessFolder(string folder, string downloadId, Artist artist, FilterFilesType filter, bool replaceExistingFiles)
{ {
DownloadClientItem downloadClientItem = null; DownloadClientItem downloadClientItem = null;
@ -186,48 +157,20 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
audioFiles.RemoveAll(l => cueFiles.Contains(l)); audioFiles.RemoveAll(l => cueFiles.Contains(l));
foreach (var cueFile in cueFiles) foreach (var cueFile in cueFiles)
{ {
// TODO move this to the disk service var cueSheet = new CueSheet(cueFile);
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; Artist artistFromCue = null;
var artistNames = ReadFieldFromCuesheet(lines, "PERFORMER"); if (!cueSheet.Performer.Empty())
if (artistNames.Count > 0)
{ {
artistFromCue = _parsingService.GetArtist(artistNames[0]); artistFromCue = _parsingService.GetArtist(cueSheet.Performer);
} }
string albumTitle = null; var audioFile = audioFiles.Find(x => x.Name == cueSheet.FileName && x.DirectoryName == cueFile.DirectoryName);
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 var parsedAlbumInfo = new ParsedAlbumInfo
{ {
AlbumTitle = albumTitle, AlbumTitle = cueSheet.Title,
ArtistName = artistFromCue.Name, ArtistName = artistFromCue.Name,
ReleaseDate = date, ReleaseDate = cueSheet.Date,
}; };
var albumsFromCue = _parsingService.GetAlbums(parsedAlbumInfo, artistFromCue); var albumsFromCue = _parsingService.GetAlbums(parsedAlbumInfo, artistFromCue);
if (albumsFromCue == null || albumsFromCue.Count == 0) if (albumsFromCue == null || albumsFromCue.Count == 0)
@ -235,17 +178,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
continue; continue;
} }
var tempAudioFiles = new List<IFileInfo> var tempAudioFiles = new List<IFileInfo> { audioFile };
{
audioFile
};
results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, cueFile.FullName)); results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, cueSheet.Title, tempAudioFiles, cueFile.FullName));
audioFiles.Remove(audioFile); audioFiles.Remove(audioFile);
} }
}
}
}
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, string.Empty));
@ -322,7 +259,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
IncludeExisting = !replaceExistingFiles, IncludeExisting = !replaceExistingFiles,
AddNewArtists = false 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, var existingItems = group.Join(decisions,
i => i.Path, i => i.Path,

View file

@ -22,7 +22,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
{ {
double dist; double dist;
string reasons; string reasons;
if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) if (item.IsSingleFileRelease)
{ {
_logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}"); _logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}");
return Decision.Accept(); return Decision.Accept();

View file

@ -17,7 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
{ {
if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) if (item.IsSingleFileRelease)
{ {
return Decision.Accept(); return Decision.Accept();
} }

View file

@ -16,7 +16,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem) public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
{ {
if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease) if (item.IsSingleFileRelease)
{ {
return Decision.Accept(); return Decision.Accept();
} }

View file

@ -105,15 +105,12 @@ namespace NzbDrone.Core.Organizer
var pattern = namingConfig.StandardTrackFormat; 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; 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 splitPatterns = pattern.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
var components = new List<string>(); var components = new List<string>();
@ -126,14 +123,14 @@ namespace NzbDrone.Core.Organizer
if (!trackFile.IsSingleFileRelease) if (!trackFile.IsSingleFileRelease)
{ {
splitPattern = FormatTrackNumberTokens(splitPattern, "", tracks); splitPattern = FormatTrackNumberTokens(splitPattern, "", tracks);
splitPattern = FormatMediumNumberTokens(splitPattern, "", tracks);
} }
splitPattern = FormatMediumNumberTokens(splitPattern, "", tracks);
AddArtistTokens(tokenHandlers, artist); AddArtistTokens(tokenHandlers, artist);
AddAlbumTokens(tokenHandlers, album); AddAlbumTokens(tokenHandlers, album);
AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber));
if (!trackFile.IsSingleFileRelease) if (!trackFile.IsSingleFileRelease)
{ {
AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber));
AddTrackTokens(tokenHandlers, tracks, artist); AddTrackTokens(tokenHandlers, tracks, artist);
AddTrackTitlePlaceholderTokens(tokenHandlers); AddTrackTitlePlaceholderTokens(tokenHandlers);
AddTrackFileTokens(tokenHandlers, trackFile); AddTrackFileTokens(tokenHandlers, trackFile);

View file

@ -61,6 +61,8 @@ namespace NzbDrone.Core.Parser.Model
{ {
return "[" + string.Join(", ", LocalTracks.Select(x => Path.GetDirectoryName(x.Path)).Distinct()) + "]"; return "[" + string.Join(", ", LocalTracks.Select(x => Path.GetDirectoryName(x.Path)).Distinct()) + "]";
} }
public bool IsSingleFileRelease => LocalTracks.All(x => x.IsSingleFileRelease == true);
} }
public class TrackMapping public class TrackMapping