mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-22 06:23:31 -07:00
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:
parent
16a3fbe25b
commit
c87efacb6a
12 changed files with 154 additions and 120 deletions
|
@ -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>
|
||||
|
|
80
src/NzbDrone.Core/MediaFiles/CueSheet.cs
Normal file
80
src/NzbDrone.Core/MediaFiles/CueSheet.cs
Normal 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 "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 cueSheet = new CueSheet(cueFile);
|
||||
|
||||
Artist artistFromCue = null;
|
||||
if (!cueSheet.Performer.Empty())
|
||||
{
|
||||
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<IFileInfo>
|
||||
{
|
||||
audioFile
|
||||
};
|
||||
|
||||
results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, cueFile.FullName));
|
||||
audioFiles.Remove(audioFile);
|
||||
}
|
||||
}
|
||||
artistFromCue = _parsingService.GetArtist(cueSheet.Performer);
|
||||
}
|
||||
|
||||
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…
Add table
Add a link
Reference in a new issue