Add support to read cuesheet file and import a single-file release.

(cherry picked from commit 506e4415d613d3752605131d0f8b63fa448ee696)
pull/4200/head
zhangdoa 7 months ago
parent 30fc3fc70a
commit 31016bca8a

@ -28,6 +28,7 @@ class TrackRow extends Component {
absoluteTrackNumber,
title,
duration,
isSingleFileRelease,
trackFilePath,
trackFileSize,
customFormats,
@ -86,7 +87,7 @@ class TrackRow extends Component {
return (
<TableRowCell key={name}>
{
trackFilePath
isSingleFileRelease ? `${trackFilePath} (Single File)` : trackFilePath
}
</TableRowCell>
);
@ -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,

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

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

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

@ -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 {
</TableRowCellButton>
<TableRowCellButton
isDisabled={!artist || !album}
isDisabled={!artist || !album || isSingleFileRelease}
title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined}
onPress={this.onSelectTrackPress}
>
@ -265,10 +267,20 @@ class InteractiveImportRow extends Component {
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
}
{
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
!isSingleFileRelease && showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
}
</TableRowCellButton>
<TableRowCell
id={id}
title={'Is Single File Release'}
>
{
isSingleFileRelease ? 'Yes' : 'No'
}
</TableRowCell>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={this.onSelectReleaseGroupPress}
@ -408,7 +420,8 @@ InteractiveImportRow.propTypes = {
artist: PropTypes.object,
album: PropTypes.object,
albumReleaseId: PropTypes.number,
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
tracks: PropTypes.arrayOf(PropTypes.object),
isSingleFileRelease: PropTypes.bool.isRequired,
releaseGroup: PropTypes.string,
quality: PropTypes.object,
size: PropTypes.number.isRequired,

@ -206,6 +206,7 @@ export const actionHandlers = handleThunks({
albumId: item.album ? item.album.id : undefined,
albumReleaseId: item.albumReleaseId ? item.albumReleaseId : undefined,
trackIds: (item.tracks || []).map((e) => e.id),
isSingleFileRelease: item.isSingleFileRelease,
quality: item.quality,
releaseGroup: item.releaseGroup,
downloadId: item.downloadId,

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

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

@ -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<Rejection> Rejections { get; set; }
}

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

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

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

@ -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<List<Track>> Tracks { get; set; }

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

@ -131,6 +131,13 @@ 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)
{
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)

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

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

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

@ -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<LocalTrack> decision = null;
if (localTrack.Tracks.Empty())
if (!localTrack.IsSingleFileRelease && localTrack.Tracks.Empty())
{
decision = localTrack.Album != null ? new ImportDecision<LocalTrack>(localTrack, new Rejection($"Couldn't parse track from: {localTrack.FileTrackInfo}")) :
new ImportDecision<LocalTrack>(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}"));

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

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

@ -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<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;
@ -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<ManualImportItem>();
// 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<IFileInfo>
{
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<ManualImportItem> ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List<IFileInfo> 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>(localTrack);
if (_rootFolderService.GetBestRootFolder(artist.Path) == null)
{

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

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

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

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

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

@ -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<string>();
@ -119,15 +122,23 @@ namespace NzbDrone.Core.Organizer
{
var splitPattern = splitPatterns[i];
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(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}", "...");

@ -73,5 +73,6 @@ namespace NzbDrone.Core.Parser.Model
public Dictionary<LocalTrack, Tuple<Track, Distance>> Mapping { get; set; }
public List<LocalTrack> LocalExtra { get; set; }
public List<Track> MBExtra { get; set; }
public bool IsSingleFileRelease { get; set; }
}
}

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

Loading…
Cancel
Save