mirror of https://github.com/lidarr/Lidarr
Add support to read cuesheet file and import a single-file release.
(cherry picked from commit 506e4415d613d3752605131d0f8b63fa448ee696)
This commit is contained in:
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,29 +179,36 @@ 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);
|
||||
}
|
||||
|
||||
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
||||
|
||||
// missing tracks
|
||||
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
||||
else
|
||||
{
|
||||
dist.Add("missing_tracks", 1.0);
|
||||
foreach (var pair in mapping.Mapping)
|
||||
{
|
||||
dist.Add("tracks", pair.Value.Item2.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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Logger.Trace("after unmatched 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);
|
||||
}
|
||||
|
||||
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…
Reference in New Issue