mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-20 05:23:31 -07:00
Merge 29e36e45ba
into 8035d4202f
This commit is contained in:
commit
0e6cda5134
35 changed files with 1006 additions and 113 deletions
|
@ -28,6 +28,7 @@ class TrackRow extends Component {
|
||||||
absoluteTrackNumber,
|
absoluteTrackNumber,
|
||||||
title,
|
title,
|
||||||
duration,
|
duration,
|
||||||
|
isSingleFileRelease,
|
||||||
trackFilePath,
|
trackFilePath,
|
||||||
trackFileSize,
|
trackFileSize,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
@ -86,7 +87,7 @@ class TrackRow extends Component {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
{
|
{
|
||||||
trackFilePath
|
isSingleFileRelease ? `${trackFilePath} (Single File)` : trackFilePath
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
|
@ -203,6 +204,7 @@ TrackRow.propTypes = {
|
||||||
absoluteTrackNumber: PropTypes.number,
|
absoluteTrackNumber: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
duration: PropTypes.number.isRequired,
|
duration: PropTypes.number.isRequired,
|
||||||
|
isSingleFileRelease: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
trackFilePath: PropTypes.string,
|
trackFilePath: PropTypes.string,
|
||||||
trackFileSize: PropTypes.number,
|
trackFileSize: PropTypes.number,
|
||||||
|
|
|
@ -13,7 +13,8 @@ function createMapStateToProps() {
|
||||||
trackFilePath: trackFile ? trackFile.path : null,
|
trackFilePath: trackFile ? trackFile.path : null,
|
||||||
trackFileSize: trackFile ? trackFile.size : null,
|
trackFileSize: trackFile ? trackFile.size : null,
|
||||||
customFormats: trackFile ? trackFile.customFormats : [],
|
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'),
|
label: () => translate('Tracks'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'cueSheetPath',
|
||||||
|
label: () => 'Cue Sheet Path',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'releaseGroup',
|
name: 'releaseGroup',
|
||||||
label: () => translate('ReleaseGroup'),
|
label: () => translate('ReleaseGroup'),
|
||||||
|
@ -435,6 +440,8 @@ class InteractiveImportModalContent extends Component {
|
||||||
allowArtistChange={allowArtistChange}
|
allowArtistChange={allowArtistChange}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onValidRowChange={this.onValidRowChange}
|
onValidRowChange={this.onValidRowChange}
|
||||||
|
isSingleFileRelease={item.isSingleFileRelease}
|
||||||
|
cueSheetPath={item.cueSheetPath}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -134,6 +134,8 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
album,
|
album,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
tracks,
|
tracks,
|
||||||
|
isSingleFileRelease,
|
||||||
|
cueSheetPath,
|
||||||
quality,
|
quality,
|
||||||
disableReleaseSwitching
|
disableReleaseSwitching
|
||||||
} = item;
|
} = item;
|
||||||
|
@ -148,7 +150,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tracks || !tracks.length) {
|
if (!(isSingleFileRelease && cueSheetPath) && (!tracks || !tracks.length)) {
|
||||||
this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' });
|
this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -164,6 +166,8 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
trackIds: _.map(tracks, 'id'),
|
trackIds: _.map(tracks, 'id'),
|
||||||
|
isSingleFileRelease: item.isSingleFileRelease,
|
||||||
|
cueSheetPath: item.cueSheetPath,
|
||||||
quality,
|
quality,
|
||||||
downloadId: this.props.downloadId,
|
downloadId: this.props.downloadId,
|
||||||
disableReleaseSwitching
|
disableReleaseSwitching
|
||||||
|
|
|
@ -167,6 +167,7 @@ class InteractiveImportRow extends Component {
|
||||||
album,
|
album,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
tracks,
|
tracks,
|
||||||
|
cueSheetPath,
|
||||||
quality,
|
quality,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
size,
|
size,
|
||||||
|
@ -267,8 +268,18 @@ class InteractiveImportRow extends Component {
|
||||||
{
|
{
|
||||||
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
|
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
|
||||||
}
|
}
|
||||||
|
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
id={id}
|
||||||
|
title={'Cue Sheet Path'}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
cueSheetPath
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCellButton
|
<TableRowCellButton
|
||||||
title={translate('ClickToChangeReleaseGroup')}
|
title={translate('ClickToChangeReleaseGroup')}
|
||||||
onPress={this.onSelectReleaseGroupPress}
|
onPress={this.onSelectReleaseGroupPress}
|
||||||
|
@ -408,7 +419,9 @@ InteractiveImportRow.propTypes = {
|
||||||
artist: PropTypes.object,
|
artist: PropTypes.object,
|
||||||
album: PropTypes.object,
|
album: PropTypes.object,
|
||||||
albumReleaseId: PropTypes.number,
|
albumReleaseId: PropTypes.number,
|
||||||
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
|
tracks: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
isSingleFileRelease: PropTypes.bool.isRequired,
|
||||||
|
cueSheetPath: PropTypes.string.isRequired,
|
||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
quality: PropTypes.object,
|
quality: PropTypes.object,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
|
|
|
@ -206,6 +206,8 @@ export const actionHandlers = handleThunks({
|
||||||
albumId: item.album ? item.album.id : undefined,
|
albumId: item.album ? item.album.id : undefined,
|
||||||
albumReleaseId: item.albumReleaseId ? item.albumReleaseId : undefined,
|
albumReleaseId: item.albumReleaseId ? item.albumReleaseId : undefined,
|
||||||
trackIds: (item.tracks || []).map((e) => e.id),
|
trackIds: (item.tracks || []).map((e) => e.id),
|
||||||
|
isSingleFileRelease: item.isSingleFileRelease,
|
||||||
|
cueSheetPath: item.cueSheetPath,
|
||||||
quality: item.quality,
|
quality: item.quality,
|
||||||
releaseGroup: item.releaseGroup,
|
releaseGroup: item.releaseGroup,
|
||||||
downloadId: item.downloadId,
|
downloadId: item.downloadId,
|
||||||
|
|
|
@ -83,7 +83,9 @@ namespace Lidarr.Api.V1.ManualImport
|
||||||
DownloadId = resource.DownloadId,
|
DownloadId = resource.DownloadId,
|
||||||
AdditionalFile = resource.AdditionalFile,
|
AdditionalFile = resource.AdditionalFile,
|
||||||
ReplaceExistingFiles = resource.ReplaceExistingFiles,
|
ReplaceExistingFiles = resource.ReplaceExistingFiles,
|
||||||
DisableReleaseSwitching = resource.DisableReleaseSwitching
|
DisableReleaseSwitching = resource.DisableReleaseSwitching,
|
||||||
|
IsSingleFileRelease = resource.IsSingleFileRelease,
|
||||||
|
CueSheetPath = resource.CueSheetPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ namespace Lidarr.Api.V1.ManualImport
|
||||||
public bool AdditionalFile { get; set; }
|
public bool AdditionalFile { get; set; }
|
||||||
public bool ReplaceExistingFiles { get; set; }
|
public bool ReplaceExistingFiles { get; set; }
|
||||||
public bool DisableReleaseSwitching { get; set; }
|
public bool DisableReleaseSwitching { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public string CueSheetPath { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ManualImportResourceMapper
|
public static class ManualImportResourceMapper
|
||||||
|
@ -52,6 +54,8 @@ namespace Lidarr.Api.V1.ManualImport
|
||||||
Tracks = model.Tracks.ToResource(),
|
Tracks = model.Tracks.ToResource(),
|
||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
ReleaseGroup = model.ReleaseGroup,
|
ReleaseGroup = model.ReleaseGroup,
|
||||||
|
IsSingleFileRelease = model.IsSingleFileRelease,
|
||||||
|
CueSheetPath = model.CueSheetPath,
|
||||||
|
|
||||||
// QualityWeight
|
// QualityWeight
|
||||||
DownloadId = model.DownloadId,
|
DownloadId = model.DownloadId,
|
||||||
|
|
|
@ -21,7 +21,8 @@ namespace Lidarr.Api.V1.ManualImport
|
||||||
public bool AdditionalFile { get; set; }
|
public bool AdditionalFile { get; set; }
|
||||||
public bool ReplaceExistingFiles { get; set; }
|
public bool ReplaceExistingFiles { get; set; }
|
||||||
public bool DisableReleaseSwitching { get; set; }
|
public bool DisableReleaseSwitching { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public string CueSheetPath { get; set; }
|
||||||
public IEnumerable<Rejection> Rejections { get; set; }
|
public IEnumerable<Rejection> Rejections { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
src/NzbDrone.Core/Datastore/Migration/078_add_flac_cue.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/078_add_flac_cue.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(078)]
|
||||||
|
public class add_flac_cue : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("TrackFiles").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
|
<PackageReference Include="Diacritics" Version="3.3.18" />
|
||||||
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
||||||
<PackageReference Include="Polly" Version="8.2.1" />
|
<PackageReference Include="Polly" Version="8.2.1" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
||||||
<PackageReference Include="Equ" Version="2.3.0" />
|
<PackageReference Include="Equ" Version="2.3.0" />
|
||||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||||
|
<PackageReference Include="UTF.Unknown" Version="2.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Common\Lidarr.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Common\Lidarr.Common.csproj" />
|
||||||
|
|
38
src/NzbDrone.Core/MediaFiles/CueSheet.cs
Normal file
38
src/NzbDrone.Core/MediaFiles/CueSheet.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.MediaFiles
|
||||||
|
{
|
||||||
|
public class CueSheet : ModelBase
|
||||||
|
{
|
||||||
|
public class IndexEntry
|
||||||
|
{
|
||||||
|
public int Key { get; set; }
|
||||||
|
public string Time { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrackEntry
|
||||||
|
{
|
||||||
|
public int Number { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<string> Performers { get; set; } = new List<string>();
|
||||||
|
public List<IndexEntry> Indices { get; set; } = new List<IndexEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FileEntry
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public IndexEntry Index { get; set; }
|
||||||
|
public List<TrackEntry> Tracks { get; set; } = new List<TrackEntry>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public List<FileEntry> Files { get; set; } = new List<FileEntry>();
|
||||||
|
public string Genre { get; set; }
|
||||||
|
public string Date { get; set; }
|
||||||
|
public string DiscID { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public List<string> Performers { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
491
src/NzbDrone.Core/MediaFiles/CueSheetService.cs
Normal file
491
src/NzbDrone.Core/MediaFiles/CueSheetService.cs
Normal file
|
@ -0,0 +1,491 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Diacritics.Extensions;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.MediaFiles.TrackImport;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using UtfUnknown;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.MediaFiles
|
||||||
|
{
|
||||||
|
public class CueSheetInfo
|
||||||
|
{
|
||||||
|
public List<IFileInfo> MusicFiles { get; set; } = new List<IFileInfo>();
|
||||||
|
public IdentificationOverrides IdOverrides { get; set; }
|
||||||
|
public CueSheet CueSheet { get; set; }
|
||||||
|
public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && (Path.GetDirectoryName(path) == Path.GetDirectoryName(CueSheet.Path)) && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name);
|
||||||
|
public CueSheet.FileEntry TryToGetFileEntryForMediaFile(string path)
|
||||||
|
{
|
||||||
|
if (CueSheet != null && CueSheet.Files.Count > 0)
|
||||||
|
{
|
||||||
|
return CueSheet.Files.Find(x => Path.GetFileName(path) == x.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ICueSheetService
|
||||||
|
{
|
||||||
|
List<ImportDecision<LocalTrack>> GetImportDecisions(ref List<IFileInfo> mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CueSheetService : ICueSheetService
|
||||||
|
{
|
||||||
|
private readonly IParsingService _parsingService;
|
||||||
|
private readonly IMakeImportDecision _importDecisionMaker;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
private static string _FileKey = "FILE";
|
||||||
|
private static string _TrackKey = "TRACK";
|
||||||
|
private static string _IndexKey = "INDEX";
|
||||||
|
private static string _GenreKey = "REM GENRE";
|
||||||
|
private static string _DateKey = "REM DATE";
|
||||||
|
private static string _DiscIdKey = "REM DISCID";
|
||||||
|
private static string _PerformerKey = "PERFORMER";
|
||||||
|
private static string _TitleKey = "TITLE";
|
||||||
|
|
||||||
|
public CueSheetService(IParsingService parsingService,
|
||||||
|
IMakeImportDecision importDecisionMaker,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_parsingService = parsingService;
|
||||||
|
_importDecisionMaker = importDecisionMaker;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PunctuationReplacer
|
||||||
|
{
|
||||||
|
private readonly Dictionary<char, char> _replacements = new Dictionary<char, char>
|
||||||
|
{
|
||||||
|
{ '‘', '\'' }, { '’', '\'' }, // Single quotes
|
||||||
|
{ '“', '"' }, { '”', '"' }, // Double quotes
|
||||||
|
{ '‹', '<' }, { '›', '>' }, // Angle quotes
|
||||||
|
{ '«', '<' }, { '»', '>' }, // Guillemets
|
||||||
|
{ '–', '-' }, { '—', '-' }, // Dashes
|
||||||
|
{ '…', '.' }, // Ellipsis
|
||||||
|
{ '¡', '!' }, { '¿', '?' }, // Inverted punctuation (Spanish)
|
||||||
|
};
|
||||||
|
|
||||||
|
public string ReplacePunctuation(string input)
|
||||||
|
{
|
||||||
|
var output = new StringBuilder(input.Length);
|
||||||
|
|
||||||
|
foreach (var c in input)
|
||||||
|
{
|
||||||
|
if (_replacements.TryGetValue(c, out var replacement))
|
||||||
|
{
|
||||||
|
output.Append(replacement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.Append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportDecision<LocalTrack>> GetImportDecisions(ref List<IFileInfo> mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config)
|
||||||
|
{
|
||||||
|
var decisions = new List<ImportDecision<LocalTrack>>();
|
||||||
|
var cueFiles = mediaFileList.Where(x => x.Extension.Equals(".cue")).ToList();
|
||||||
|
if (cueFiles.Count == 0)
|
||||||
|
{
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaFileList.RemoveAll(l => cueFiles.Contains(l));
|
||||||
|
var cueSheetInfos = new List<CueSheetInfo>();
|
||||||
|
foreach (var cueFile in cueFiles)
|
||||||
|
{
|
||||||
|
var cueSheetInfo = GetCueSheetInfo(cueFile, mediaFileList, itemInfo.DetectCueFileEncoding);
|
||||||
|
if (idOverrides != null)
|
||||||
|
{
|
||||||
|
cueSheetInfo.IdOverrides = idOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addedCueSheetInfo = cueSheetInfos.Find(existingCueSheetInfo => existingCueSheetInfo.CueSheet.DiscID == cueSheetInfo.CueSheet.DiscID);
|
||||||
|
if (addedCueSheetInfo == null)
|
||||||
|
{
|
||||||
|
cueSheetInfos.Add(cueSheetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are multiple cue sheet files for the same disc, then we try to keep the last one or the one with the exact same name as the media file, if there's any
|
||||||
|
else if (cueSheetInfo.CueSheet.IsSingleFileRelease && addedCueSheetInfo.CueSheet.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var mediaFileName = Path.GetFileName(addedCueSheetInfo.CueSheet.Files[0].Name);
|
||||||
|
var cueSheetFileName = Path.GetFileName(cueFile.Name);
|
||||||
|
|
||||||
|
if (mediaFileName != cueSheetFileName)
|
||||||
|
{
|
||||||
|
cueSheetInfos.Remove(addedCueSheetInfo);
|
||||||
|
cueSheetInfos.Add(cueSheetInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cueSheetInfosGroupedByDiscId = cueSheetInfos.GroupBy(x => x.CueSheet.DiscID).ToList();
|
||||||
|
foreach (var cueSheetInfoGroup in cueSheetInfosGroupedByDiscId)
|
||||||
|
{
|
||||||
|
var audioFilesForCues = new List<IFileInfo>();
|
||||||
|
foreach (var cueSheetInfo in cueSheetInfoGroup)
|
||||||
|
{
|
||||||
|
audioFilesForCues.AddRange(cueSheetInfo.MusicFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemInfoWithCueSheetInfos = itemInfo;
|
||||||
|
itemInfoWithCueSheetInfos.CueSheetInfos = cueSheetInfoGroup.ToList();
|
||||||
|
decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFilesForCues, cueSheetInfoGroup.First().IdOverrides, itemInfoWithCueSheetInfos, config));
|
||||||
|
|
||||||
|
foreach (var cueSheetInfo in cueSheetInfos)
|
||||||
|
{
|
||||||
|
if (cueSheetInfo.CueSheet != null)
|
||||||
|
{
|
||||||
|
decisions.ForEach(item =>
|
||||||
|
{
|
||||||
|
if (cueSheetInfo.IsForMediaFile(item.Item.Path))
|
||||||
|
{
|
||||||
|
item.Item.CueSheetPath = cueSheetInfo.CueSheet.Path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaFileList.RemoveAll(x => cueSheetInfo.MusicFiles.Contains(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decisions.ForEach(decision =>
|
||||||
|
{
|
||||||
|
if (!decision.Item.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cueSheetFindResult = cueSheetInfos.Find(x => x.IsForMediaFile(decision.Item.Path));
|
||||||
|
var cueSheet = cueSheetFindResult?.CueSheet;
|
||||||
|
if (cueSheet == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cueSheet.Files.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracksFromCueSheet = cueSheet.Files.SelectMany(x => x.Tracks).ToList();
|
||||||
|
if (tracksFromCueSheet.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.Item.Release == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracksFromRelease = decision.Item.Release.Tracks.Value;
|
||||||
|
if (tracksFromRelease.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacer = new PunctuationReplacer();
|
||||||
|
var i = 0;
|
||||||
|
while (i < tracksFromRelease.Count)
|
||||||
|
{
|
||||||
|
var trackFromRelease = tracksFromRelease[i];
|
||||||
|
var trackFromReleaseTitle = NormalizeTitle(replacer, trackFromRelease.Title);
|
||||||
|
|
||||||
|
var j = 0;
|
||||||
|
var anyMatch = false;
|
||||||
|
while (j < tracksFromCueSheet.Count)
|
||||||
|
{
|
||||||
|
var trackFromCueSheet = tracksFromCueSheet[j];
|
||||||
|
var trackFromCueSheetTitle = NormalizeTitle(replacer, trackFromCueSheet.Title);
|
||||||
|
anyMatch = string.Equals(trackFromReleaseTitle, trackFromCueSheetTitle, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
if (anyMatch)
|
||||||
|
{
|
||||||
|
decision.Item.Tracks.Add(trackFromRelease);
|
||||||
|
tracksFromRelease.RemoveAt(i);
|
||||||
|
tracksFromCueSheet.RemoveAt(j);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyMatch)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeTitle(PunctuationReplacer replacer, string title)
|
||||||
|
{
|
||||||
|
title.Normalize(NormalizationForm.FormKD);
|
||||||
|
title = title.RemoveDiacritics();
|
||||||
|
title = replacer.ReplacePunctuation(title);
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CueSheet LoadCueSheet(IFileInfo fileInfo, bool detectCueFileEncoding)
|
||||||
|
{
|
||||||
|
using (var fs = fileInfo.OpenRead())
|
||||||
|
{
|
||||||
|
var bytes = new byte[fileInfo.Length];
|
||||||
|
while (fs.Read(bytes, 0, bytes.Length) > 0)
|
||||||
|
{
|
||||||
|
string content;
|
||||||
|
if (detectCueFileEncoding)
|
||||||
|
{
|
||||||
|
var result = CharsetDetector.DetectFromFile(fileInfo.FullName); // or pass FileInfo
|
||||||
|
var encoding = result.Detected.Encoding;
|
||||||
|
_logger.Debug("Detected encoding {0} for {1}", encoding.WebName, fileInfo.FullName);
|
||||||
|
content = encoding.GetString(bytes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content = Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
|
||||||
|
var cueSheet = ParseLines(lines);
|
||||||
|
|
||||||
|
// Single-file cue means it's an unsplit image, which should be specially treated in the pipeline
|
||||||
|
cueSheet.IsSingleFileRelease = cueSheet.Files.Count == 1;
|
||||||
|
cueSheet.Path = fileInfo.FullName;
|
||||||
|
|
||||||
|
return cueSheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CueSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ExtractValue(string line, string keyword)
|
||||||
|
{
|
||||||
|
var pattern = keyword + @"\s+(?:(?:\""(.*?)\"")|(.+))";
|
||||||
|
var match = Regex.Match(line, pattern);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var value = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ExtractPerformers(string line)
|
||||||
|
{
|
||||||
|
var delimiters = new char[] { ',', ';' };
|
||||||
|
var performers = ExtractValue(line, _PerformerKey);
|
||||||
|
return performers.Split(delimiters, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool GetNewLine(ref int index, ref string newLine, string[] lines)
|
||||||
|
{
|
||||||
|
if (index < lines.Length)
|
||||||
|
{
|
||||||
|
newLine = lines[index];
|
||||||
|
index++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CueSheet ParseLines(string[] lines)
|
||||||
|
{
|
||||||
|
var cueSheet = new CueSheet();
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
string line = null;
|
||||||
|
|
||||||
|
while (GetNewLine(ref i, ref line, lines))
|
||||||
|
{
|
||||||
|
if (line.StartsWith(_FileKey))
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
line = line.Substring(_FileKey.Length).Trim();
|
||||||
|
var filename = line.Split('"')[1];
|
||||||
|
var fileDetails = new CueSheet.FileEntry { Name = filename };
|
||||||
|
|
||||||
|
if (!GetNewLine(ref i, ref line, lines))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (line.StartsWith(" "))
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
if (line.StartsWith(_TrackKey))
|
||||||
|
{
|
||||||
|
line = line.Substring(_TrackKey.Length).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackDetails = new CueSheet.TrackEntry();
|
||||||
|
var trackInfo = line.Split(' ');
|
||||||
|
if (trackInfo.Length > 0)
|
||||||
|
{
|
||||||
|
if (int.TryParse(trackInfo[0], out var number))
|
||||||
|
{
|
||||||
|
trackDetails.Number = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetNewLine(ref i, ref line, lines))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (line.StartsWith(" "))
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
if (line.StartsWith(_IndexKey))
|
||||||
|
{
|
||||||
|
line = line.Substring(_IndexKey.Length).Trim();
|
||||||
|
var parts = line.Split(' ');
|
||||||
|
if (parts.Length > 1)
|
||||||
|
{
|
||||||
|
if (int.TryParse(parts[0], out var key))
|
||||||
|
{
|
||||||
|
var value = parts[1].Trim('"');
|
||||||
|
trackDetails.Indices.Add(new CueSheet.IndexEntry { Key = key, Time = value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_TitleKey))
|
||||||
|
{
|
||||||
|
trackDetails.Title = ExtractValue(line, _TitleKey);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_PerformerKey))
|
||||||
|
{
|
||||||
|
trackDetails.Performers = ExtractPerformers(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetNewLine(ref i, ref line, lines))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileDetails.Tracks.Add(trackDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
cueSheet.Files.Add(fileDetails);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_GenreKey))
|
||||||
|
{
|
||||||
|
cueSheet.Genre = ExtractValue(line, _GenreKey);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_DateKey))
|
||||||
|
{
|
||||||
|
cueSheet.Date = ExtractValue(line, _DateKey);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_DiscIdKey))
|
||||||
|
{
|
||||||
|
cueSheet.DiscID = ExtractValue(line, _DiscIdKey);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_PerformerKey))
|
||||||
|
{
|
||||||
|
cueSheet.Performers = ExtractPerformers(line);
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(_TitleKey))
|
||||||
|
{
|
||||||
|
cueSheet.Title = ExtractValue(line, _TitleKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cueSheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Artist GetArtist(List<string> performers)
|
||||||
|
{
|
||||||
|
if (performers.Count == 1)
|
||||||
|
{
|
||||||
|
return _parsingService.GetArtist(performers[0]);
|
||||||
|
}
|
||||||
|
else if (performers.Count > 1)
|
||||||
|
{
|
||||||
|
return _parsingService.GetArtist("various artists");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CueSheetInfo GetCueSheetInfo(IFileInfo cueFile, List<IFileInfo> musicFiles, bool detectCueFileEncoding)
|
||||||
|
{
|
||||||
|
var cueSheetInfo = new CueSheetInfo();
|
||||||
|
var cueSheet = LoadCueSheet(cueFile, detectCueFileEncoding);
|
||||||
|
if (cueSheet == null)
|
||||||
|
{
|
||||||
|
return cueSheetInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
cueSheetInfo.CueSheet = cueSheet;
|
||||||
|
var musicFilesInTheSameDir = musicFiles.Where(musicFile => musicFile.DirectoryName == Path.GetDirectoryName(cueSheetInfo.CueSheet.Path)).ToList();
|
||||||
|
cueSheetInfo.MusicFiles = musicFilesInTheSameDir.Where(musicFile => cueSheet.Files.Any(musicFileFromCue => musicFileFromCue.Name == musicFile.Name)).ToList();
|
||||||
|
|
||||||
|
cueSheetInfo.IdOverrides = new IdentificationOverrides();
|
||||||
|
|
||||||
|
var artistFromCue = GetArtist(cueSheet.Performers);
|
||||||
|
|
||||||
|
if (artistFromCue == null && cueSheet.Files.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var fileEntry in cueSheet.Files)
|
||||||
|
{
|
||||||
|
foreach (var track in fileEntry.Tracks)
|
||||||
|
{
|
||||||
|
artistFromCue = GetArtist(track.Performers);
|
||||||
|
if (artistFromCue != null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cue sheet file is too incomplete in this case
|
||||||
|
if (artistFromCue == null)
|
||||||
|
{
|
||||||
|
return cueSheetInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
cueSheetInfo.IdOverrides.Artist = artistFromCue;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
cueSheetInfo.IdOverrides.Album = albumsFromCue[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return cueSheetInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,12 +11,14 @@ using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.MediaFiles.Commands;
|
using NzbDrone.Core.MediaFiles.Commands;
|
||||||
using NzbDrone.Core.MediaFiles.Events;
|
using NzbDrone.Core.MediaFiles.Events;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport;
|
using NzbDrone.Core.MediaFiles.TrackImport;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.RootFolders;
|
using NzbDrone.Core.RootFolders;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles
|
namespace NzbDrone.Core.MediaFiles
|
||||||
|
@ -41,6 +43,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IMediaFileService _mediaFileService;
|
private readonly IMediaFileService _mediaFileService;
|
||||||
private readonly IMakeImportDecision _importDecisionMaker;
|
private readonly IMakeImportDecision _importDecisionMaker;
|
||||||
|
private readonly ICueSheetService _cueSheetService;
|
||||||
private readonly IImportApprovedTracks _importApprovedTracks;
|
private readonly IImportApprovedTracks _importApprovedTracks;
|
||||||
private readonly IArtistService _artistService;
|
private readonly IArtistService _artistService;
|
||||||
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
|
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
|
||||||
|
@ -52,6 +55,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IMediaFileService mediaFileService,
|
IMediaFileService mediaFileService,
|
||||||
IMakeImportDecision importDecisionMaker,
|
IMakeImportDecision importDecisionMaker,
|
||||||
|
ICueSheetService cueSheetService,
|
||||||
IImportApprovedTracks importApprovedTracks,
|
IImportApprovedTracks importApprovedTracks,
|
||||||
IArtistService artistService,
|
IArtistService artistService,
|
||||||
IRootFolderService rootFolderService,
|
IRootFolderService rootFolderService,
|
||||||
|
@ -63,6 +67,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_mediaFileService = mediaFileService;
|
_mediaFileService = mediaFileService;
|
||||||
_importDecisionMaker = importDecisionMaker;
|
_importDecisionMaker = importDecisionMaker;
|
||||||
|
_cueSheetService = cueSheetService;
|
||||||
_importApprovedTracks = importApprovedTracks;
|
_importApprovedTracks = importApprovedTracks;
|
||||||
_artistService = artistService;
|
_artistService = artistService;
|
||||||
_mediaFileTableCleanupService = mediaFileTableCleanupService;
|
_mediaFileTableCleanupService = mediaFileTableCleanupService;
|
||||||
|
@ -83,6 +88,32 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
artistIds = new List<int>();
|
artistIds = new List<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mediaFileList = GetMediaFiles(folders, artistIds);
|
||||||
|
|
||||||
|
var decisionsStopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
var itemInfo = new ImportDecisionMakerInfo();
|
||||||
|
var config = new ImportDecisionMakerConfig
|
||||||
|
{
|
||||||
|
Filter = filter,
|
||||||
|
IncludeExisting = true,
|
||||||
|
AddNewArtists = addNewArtists
|
||||||
|
};
|
||||||
|
|
||||||
|
var decisions = new List<ImportDecision<LocalTrack>>();
|
||||||
|
|
||||||
|
itemInfo.DetectCueFileEncoding = false;
|
||||||
|
decisions.AddRange(_cueSheetService.GetImportDecisions(ref mediaFileList, null, itemInfo, config));
|
||||||
|
decisions.AddRange(_importDecisionMaker.GetImportDecisions(mediaFileList, null, itemInfo, config));
|
||||||
|
|
||||||
|
decisionsStopwatch.Stop();
|
||||||
|
_logger.Debug("Import decisions complete [{0}]", decisionsStopwatch.Elapsed);
|
||||||
|
|
||||||
|
Import(folders, artistIds, decisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IFileInfo> GetMediaFiles(List<string> folders, List<int> artistIds)
|
||||||
|
{
|
||||||
var mediaFileList = new List<IFileInfo>();
|
var mediaFileList = new List<IFileInfo>();
|
||||||
|
|
||||||
var musicFilesStopwatch = Stopwatch.StartNew();
|
var musicFilesStopwatch = Stopwatch.StartNew();
|
||||||
|
@ -96,7 +127,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
if (rootFolder == null)
|
if (rootFolder == null)
|
||||||
{
|
{
|
||||||
_logger.Error("Not scanning {0}, it's not a subdirectory of a defined root folder", folder);
|
_logger.Error("Not scanning {0}, it's not a subdirectory of a defined root folder", folder);
|
||||||
return;
|
return mediaFileList;
|
||||||
}
|
}
|
||||||
|
|
||||||
var folderExists = _diskProvider.FolderExists(folder);
|
var folderExists = _diskProvider.FolderExists(folder);
|
||||||
|
@ -108,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_logger.Warn("Artists' root folder ({0}) doesn't exist.", rootFolder.Path);
|
_logger.Warn("Artists' root folder ({0}) doesn't exist.", rootFolder.Path);
|
||||||
var skippedArtists = _artistService.GetArtists(artistIds);
|
var skippedArtists = _artistService.GetArtists(artistIds);
|
||||||
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist)));
|
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist)));
|
||||||
return;
|
return mediaFileList;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_diskProvider.FolderEmpty(rootFolder.Path))
|
if (_diskProvider.FolderEmpty(rootFolder.Path))
|
||||||
|
@ -116,7 +147,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_logger.Warn("Artists' root folder ({0}) is empty.", rootFolder.Path);
|
_logger.Warn("Artists' root folder ({0}) is empty.", rootFolder.Path);
|
||||||
var skippedArtists = _artistService.GetArtists(artistIds);
|
var skippedArtists = _artistService.GetArtists(artistIds);
|
||||||
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderIsEmpty)));
|
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderIsEmpty)));
|
||||||
return;
|
return mediaFileList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,20 +176,11 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
musicFilesStopwatch.Stop();
|
musicFilesStopwatch.Stop();
|
||||||
_logger.Trace("Finished getting track files for:\n{0} [{1}]", folders.ConcatToString("\n"), musicFilesStopwatch.Elapsed);
|
_logger.Trace("Finished getting track files for:\n{0} [{1}]", folders.ConcatToString("\n"), musicFilesStopwatch.Elapsed);
|
||||||
|
|
||||||
var decisionsStopwatch = Stopwatch.StartNew();
|
return mediaFileList;
|
||||||
|
}
|
||||||
var config = new ImportDecisionMakerConfig
|
|
||||||
{
|
|
||||||
Filter = filter,
|
|
||||||
IncludeExisting = true,
|
|
||||||
AddNewArtists = addNewArtists
|
|
||||||
};
|
|
||||||
|
|
||||||
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, null, null, config);
|
|
||||||
|
|
||||||
decisionsStopwatch.Stop();
|
|
||||||
_logger.Debug("Import decisions complete [{0}]", decisionsStopwatch.Elapsed);
|
|
||||||
|
|
||||||
|
private void Import(List<string> folders, List<int> artistIds, List<ImportDecision<LocalTrack>> decisions)
|
||||||
|
{
|
||||||
var importStopwatch = Stopwatch.StartNew();
|
var importStopwatch = Stopwatch.StartNew();
|
||||||
_importApprovedTracks.Import(decisions, false);
|
_importApprovedTracks.Import(decisions, false);
|
||||||
|
|
||||||
|
@ -177,7 +199,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
Modified = decision.Item.Modified,
|
Modified = decision.Item.Modified,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
Quality = decision.Item.Quality,
|
Quality = decision.Item.Quality,
|
||||||
MediaInfo = decision.Item.FileTrackInfo.MediaInfo
|
MediaInfo = decision.Item.FileTrackInfo.MediaInfo,
|
||||||
|
IsSingleFileRelease = decision.Item.IsSingleFileRelease,
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
_mediaFileService.AddMany(newFiles);
|
_mediaFileService.AddMany(newFiles);
|
||||||
|
|
|
@ -27,7 +27,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
{ ".ape", Quality.APE },
|
{ ".ape", Quality.APE },
|
||||||
{ ".aif", Quality.Unknown },
|
{ ".aif", Quality.Unknown },
|
||||||
{ ".aiff", 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 QualityModel Quality { get; set; }
|
||||||
public MediaInfoModel MediaInfo { get; set; }
|
public MediaInfoModel MediaInfo { get; set; }
|
||||||
public int AlbumId { get; set; }
|
public int AlbumId { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
|
||||||
// These are queried from the database
|
// These are queried from the database
|
||||||
public LazyLoaded<List<Track>> Tracks { get; set; }
|
public LazyLoaded<List<Track>> Tracks { get; set; }
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.MediaFiles.Events;
|
using NzbDrone.Core.MediaFiles.Events;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport;
|
using NzbDrone.Core.MediaFiles.TrackImport;
|
||||||
|
@ -79,6 +80,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
|
|
||||||
EnsureTrackFolder(trackFile, localTrack, filePath);
|
EnsureTrackFolder(trackFile, localTrack, filePath);
|
||||||
|
|
||||||
|
TryToCreateCueFile(localTrack, filePath);
|
||||||
|
|
||||||
_logger.Debug("Moving track file: {0} to {1}", trackFile.Path, filePath);
|
_logger.Debug("Moving track file: {0} to {1}", trackFile.Path, filePath);
|
||||||
|
|
||||||
return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move);
|
return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Move);
|
||||||
|
@ -90,6 +93,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
|
|
||||||
EnsureTrackFolder(trackFile, localTrack, filePath);
|
EnsureTrackFolder(trackFile, localTrack, filePath);
|
||||||
|
|
||||||
|
TryToCreateCueFile(localTrack, filePath);
|
||||||
|
|
||||||
if (_configService.CopyUsingHardlinks)
|
if (_configService.CopyUsingHardlinks)
|
||||||
{
|
{
|
||||||
_logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath);
|
_logger.Debug("Attempting to hardlink track file: {0} to {1}", trackFile.Path, filePath);
|
||||||
|
@ -100,6 +105,24 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy);
|
return TransferFile(trackFile, localTrack.Artist, localTrack.Tracks, filePath, TransferMode.Copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryToCreateCueFile(LocalTrack localTrack, string trackFilePath)
|
||||||
|
{
|
||||||
|
if (localTrack.IsSingleFileRelease && !localTrack.CueSheetPath.Empty())
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(trackFilePath);
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(trackFilePath);
|
||||||
|
var cueSheetPath = Path.Combine(directory, fileName + ".cue");
|
||||||
|
_diskTransferService.TransferFile(localTrack.CueSheetPath, cueSheetPath, TransferMode.Copy, true);
|
||||||
|
var lines = new List<string>(File.ReadAllLines(cueSheetPath));
|
||||||
|
var fileLineIndex = lines.FindIndex(line => line.Contains("FILE"));
|
||||||
|
if (fileLineIndex != -1)
|
||||||
|
{
|
||||||
|
lines[fileLineIndex] = "FILE \"" + Path.GetFileName(trackFilePath) + "\" WAVE";
|
||||||
|
File.WriteAllLines(cueSheetPath, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private TrackFile TransferFile(TrackFile trackFile, Artist artist, List<Track> tracks, string destinationFilePath, TransferMode mode)
|
private TrackFile TransferFile(TrackFile trackFile, Artist artist, List<Track> tracks, string destinationFilePath, TransferMode mode)
|
||||||
{
|
{
|
||||||
Ensure.That(trackFile, () => trackFile).IsNotNull();
|
Ensure.That(trackFile, () => trackFile).IsNotNull();
|
||||||
|
|
|
@ -65,14 +65,28 @@ 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");
|
||||||
foreach (var charSep in CharsAndSeps)
|
if (release.IsSingleFileRelease)
|
||||||
{
|
{
|
||||||
foreach (var pattern in Patterns(charSep.Item1, charSep.Item2))
|
for (var i = 0; i < tracks.Count; ++i)
|
||||||
{
|
{
|
||||||
var matches = AllMatches(tracks, pattern);
|
tracks[i].FileTrackInfo.ArtistTitle = tracks[i].Artist?.Name;
|
||||||
if (matches != null)
|
tracks[i].FileTrackInfo.AlbumTitle = tracks[i].Album?.Title;
|
||||||
|
|
||||||
|
// 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?.Year ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var charSep in CharsAndSeps)
|
||||||
|
{
|
||||||
|
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)
|
private List<CandidateAlbumRelease> GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting)
|
||||||
{
|
{
|
||||||
|
if (localAlbumRelease.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
|
// sort candidate releases by closest track count so that we stand a chance of
|
||||||
// getting a perfect match early on
|
// getting a perfect match early on
|
||||||
return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
|
return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
|
||||||
|
|
|
@ -30,6 +30,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber;
|
localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TrackIndexIncorrect(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber)
|
||||||
|
{
|
||||||
|
return cuesheetTrack.Number != mbTrack.AbsoluteTrackNumber;
|
||||||
|
}
|
||||||
|
|
||||||
public static int GetTotalTrackNumber(Track track, List<Track> allTracks)
|
public static int GetTotalTrackNumber(Track track, List<Track> allTracks)
|
||||||
{
|
{
|
||||||
return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber);
|
return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber);
|
||||||
|
@ -79,6 +84,28 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
return dist;
|
return dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Distance TrackDistance(CueSheet.TrackEntry cuesheetTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false)
|
||||||
|
{
|
||||||
|
var dist = new Distance();
|
||||||
|
|
||||||
|
// musicbrainz never has 'featuring' in the track title
|
||||||
|
// see https://musicbrainz.org/doc/Style/Artist_Credits
|
||||||
|
dist.AddString("track_title", cuesheetTrack.Title ?? "", mbTrack.Title);
|
||||||
|
|
||||||
|
if (includeArtist && cuesheetTrack.Performers.Count == 1
|
||||||
|
&& !VariousArtistNames.Any(x => x.Equals(cuesheetTrack.Performers[0], StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
dist.AddString("track_artist", cuesheetTrack.Performers[0], mbTrack.ArtistMetadata.Value.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbTrack.AbsoluteTrackNumber > 0)
|
||||||
|
{
|
||||||
|
dist.AddBool("track_index", TrackIndexIncorrect(cuesheetTrack, mbTrack, totalTrackNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
public static Distance AlbumReleaseDistance(List<LocalTrack> localTracks, AlbumRelease release, TrackMapping mapping)
|
public static Distance AlbumReleaseDistance(List<LocalTrack> localTracks, AlbumRelease release, TrackMapping mapping)
|
||||||
{
|
{
|
||||||
var dist = new Distance();
|
var dist = new Distance();
|
||||||
|
@ -118,13 +145,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
{
|
{
|
||||||
var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0;
|
var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0;
|
||||||
var releaseYear = release.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.All(x => x.IsSingleFileRelease == true) ? false : localYear == albumYear;
|
||||||
|
if (isSameWithAlbumYear || localYear == releaseYear)
|
||||||
{
|
{
|
||||||
dist.Add("year", 0.0);
|
dist.Add("year", 0.0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var remoteYear = albumYear > 0 ? albumYear : releaseYear;
|
var remoteYear = (albumYear > 0 && isSameWithAlbumYear) ? albumYear : releaseYear;
|
||||||
var diff = Math.Abs(localYear - remoteYear);
|
var diff = Math.Abs(localYear - remoteYear);
|
||||||
var diff_max = Math.Abs(DateTime.Now.Year - remoteYear);
|
var diff_max = Math.Abs(DateTime.Now.Year - remoteYear);
|
||||||
dist.AddRatio("year", diff, diff_max);
|
dist.AddRatio("year", diff, diff_max);
|
||||||
|
@ -176,12 +206,32 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracks
|
// tracks
|
||||||
foreach (var pair in mapping.Mapping)
|
if (mapping.CuesheetTrackMapping.Count != 0)
|
||||||
{
|
{
|
||||||
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
foreach (var pair in mapping.CuesheetTrackMapping)
|
||||||
}
|
{
|
||||||
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
||||||
|
}
|
||||||
|
|
||||||
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var pair in mapping.Mapping)
|
||||||
|
{
|
||||||
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Trace("after trackMapping: {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());
|
||||||
|
}
|
||||||
|
|
||||||
// missing tracks
|
// missing tracks
|
||||||
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
||||||
|
@ -191,14 +241,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
|
|
||||||
|
|
||||||
return dist;
|
return dist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,6 +154,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
|
|
||||||
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
|
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
|
||||||
{
|
{
|
||||||
|
if (localAlbumRelease.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var worstTrackMatchDist = localAlbumRelease.TrackMapping?.Mapping
|
var worstTrackMatchDist = localAlbumRelease.TrackMapping?.Mapping
|
||||||
.DefaultIfEmpty()
|
.DefaultIfEmpty()
|
||||||
.MaxBy(x => x.Value.Item2.NormalizedDistance())
|
.MaxBy(x => x.Value.Item2.NormalizedDistance())
|
||||||
|
@ -182,7 +187,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
FileTrackInfo = _audioTagService.ReadTags(x.Path),
|
FileTrackInfo = _audioTagService.ReadTags(x.Path),
|
||||||
ExistingFile = true,
|
ExistingFile = true,
|
||||||
AdditionalFile = true,
|
AdditionalFile = true,
|
||||||
Quality = x.Quality
|
Quality = x.Quality,
|
||||||
|
IsSingleFileRelease = x.IsSingleFileRelease,
|
||||||
}))
|
}))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -317,7 +323,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList();
|
var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList();
|
||||||
var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList();
|
var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList();
|
||||||
|
|
||||||
var mapping = MapReleaseTracks(allLocalTracks, release.Tracks.Value);
|
var isSingleFileRelease = allLocalTracks.All(x => x.IsSingleFileRelease == true);
|
||||||
|
var mapping = isSingleFileRelease ? MapSingleFileReleaseTracks(allLocalTracks, release.Tracks.Value) : MapReleaseTracks(allLocalTracks, release.Tracks.Value);
|
||||||
var distance = DistanceCalculator.AlbumReleaseDistance(allLocalTracks, release, mapping);
|
var distance = DistanceCalculator.AlbumReleaseDistance(allLocalTracks, release, mapping);
|
||||||
var currDistance = distance.NormalizedDistance();
|
var currDistance = distance.NormalizedDistance();
|
||||||
|
|
||||||
|
@ -335,6 +342,7 @@ 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 (currDistance == 0.0)
|
if (currDistance == 0.0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
@ -348,6 +356,7 @@ 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 distances = new Distance[localTracks.Count, mbTracks.Count];
|
var distances = new Distance[localTracks.Count, mbTracks.Count];
|
||||||
var costs = new double[localTracks.Count, mbTracks.Count];
|
var costs = new double[localTracks.Count, mbTracks.Count];
|
||||||
|
|
||||||
|
@ -364,7 +373,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
var m = new Munkres(costs);
|
var m = new Munkres(costs);
|
||||||
m.Run();
|
m.Run();
|
||||||
|
|
||||||
var result = new TrackMapping();
|
|
||||||
foreach (var pair in m.Solution)
|
foreach (var pair in m.Solution)
|
||||||
{
|
{
|
||||||
result.Mapping.Add(localTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2]));
|
result.Mapping.Add(localTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2]));
|
||||||
|
@ -379,5 +387,46 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TrackMapping MapSingleFileReleaseTracks(List<LocalTrack> localTracks, List<Track> mbTracks)
|
||||||
|
{
|
||||||
|
var result = new TrackMapping();
|
||||||
|
|
||||||
|
var cuesheetTracks = new List<CueSheet.TrackEntry>();
|
||||||
|
foreach (var localTrack in localTracks)
|
||||||
|
{
|
||||||
|
if (localTrack.CueSheetFileEntry != null)
|
||||||
|
{
|
||||||
|
cuesheetTracks.AddRange(localTrack.CueSheetFileEntry.Tracks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var distances = new Distance[cuesheetTracks.Count, mbTracks.Count];
|
||||||
|
var costs = new double[cuesheetTracks.Count, mbTracks.Count];
|
||||||
|
|
||||||
|
for (var col = 0; col < mbTracks.Count; col++)
|
||||||
|
{
|
||||||
|
var totalTrackNumber = DistanceCalculator.GetTotalTrackNumber(mbTracks[col], mbTracks);
|
||||||
|
for (var row = 0; row < cuesheetTracks.Count; row++)
|
||||||
|
{
|
||||||
|
distances[row, col] = DistanceCalculator.TrackDistance(cuesheetTracks[row], mbTracks[col], totalTrackNumber, false);
|
||||||
|
costs[row, col] = distances[row, col].NormalizedDistance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = new Munkres(costs);
|
||||||
|
m.Run();
|
||||||
|
|
||||||
|
foreach (var pair in m.Solution)
|
||||||
|
{
|
||||||
|
result.CuesheetTrackMapping.Add(cuesheetTracks[pair.Item1], Tuple.Create(mbTracks[pair.Item2], distances[pair.Item1, pair.Item2]));
|
||||||
|
_logger.Trace("Mapped {0} to {1}, dist: {2}", cuesheetTracks[pair.Item1], mbTracks[pair.Item2], costs[pair.Item1, pair.Item2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.MBExtra = mbTracks.Except(result.CuesheetTrackMapping.Values.Select(x => x.Item1)).ToList();
|
||||||
|
_logger.Trace($"Missing tracks:\n{string.Join("\n", result.MBExtra)}");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,7 +194,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
AlbumId = localTrack.Album.Id,
|
AlbumId = localTrack.Album.Id,
|
||||||
Artist = localTrack.Artist,
|
Artist = localTrack.Artist,
|
||||||
Album = localTrack.Album,
|
Album = localTrack.Album,
|
||||||
Tracks = localTrack.Tracks
|
Tracks = localTrack.Tracks,
|
||||||
|
IsSingleFileRelease = localTrack.IsSingleFileRelease,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool copyOnly;
|
bool copyOnly;
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using DryIoc.ImTools;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
@ -10,6 +11,7 @@ using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
|
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.RootFolders;
|
using NzbDrone.Core.RootFolders;
|
||||||
|
@ -32,6 +34,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
{
|
{
|
||||||
public DownloadClientItem DownloadClientItem { get; set; }
|
public DownloadClientItem DownloadClientItem { get; set; }
|
||||||
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
|
public ParsedAlbumInfo ParsedAlbumInfo { get; set; }
|
||||||
|
public List<CueSheetInfo> CueSheetInfos { get; set; } = new List<CueSheetInfo>();
|
||||||
|
public bool DetectCueFileEncoding { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImportDecisionMakerConfig
|
public class ImportDecisionMakerConfig
|
||||||
|
@ -48,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> _trackSpecifications;
|
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> _trackSpecifications;
|
||||||
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumSpecifications;
|
private readonly IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumSpecifications;
|
||||||
private readonly IMediaFileService _mediaFileService;
|
private readonly IMediaFileService _mediaFileService;
|
||||||
|
private readonly IParsingService _parsingService;
|
||||||
private readonly IAudioTagService _audioTagService;
|
private readonly IAudioTagService _audioTagService;
|
||||||
private readonly IAugmentingService _augmentingService;
|
private readonly IAugmentingService _augmentingService;
|
||||||
private readonly IIdentificationService _identificationService;
|
private readonly IIdentificationService _identificationService;
|
||||||
|
@ -58,6 +63,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> trackSpecifications,
|
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> trackSpecifications,
|
||||||
IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> albumSpecifications,
|
IEnumerable<IImportDecisionEngineSpecification<LocalAlbumRelease>> albumSpecifications,
|
||||||
IMediaFileService mediaFileService,
|
IMediaFileService mediaFileService,
|
||||||
|
IParsingService parsingService,
|
||||||
IAudioTagService audioTagService,
|
IAudioTagService audioTagService,
|
||||||
IAugmentingService augmentingService,
|
IAugmentingService augmentingService,
|
||||||
IIdentificationService identificationService,
|
IIdentificationService identificationService,
|
||||||
|
@ -68,6 +74,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
_trackSpecifications = trackSpecifications;
|
_trackSpecifications = trackSpecifications;
|
||||||
_albumSpecifications = albumSpecifications;
|
_albumSpecifications = albumSpecifications;
|
||||||
_mediaFileService = mediaFileService;
|
_mediaFileService = mediaFileService;
|
||||||
|
_parsingService = parsingService;
|
||||||
_audioTagService = audioTagService;
|
_audioTagService = audioTagService;
|
||||||
_augmentingService = augmentingService;
|
_augmentingService = augmentingService;
|
||||||
_identificationService = identificationService;
|
_identificationService = identificationService;
|
||||||
|
@ -113,7 +120,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
Size = file.Length,
|
Size = file.Length,
|
||||||
Modified = file.LastWriteTimeUtc,
|
Modified = file.LastWriteTimeUtc,
|
||||||
FileTrackInfo = _audioTagService.ReadTags(file.FullName),
|
FileTrackInfo = _audioTagService.ReadTags(file.FullName),
|
||||||
AdditionalFile = false
|
AdditionalFile = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -149,6 +156,38 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
var decisions = trackData.Item2;
|
var decisions = trackData.Item2;
|
||||||
|
|
||||||
localTracks.ForEach(x => x.ExistingFile = !config.NewDownload);
|
localTracks.ForEach(x => x.ExistingFile = !config.NewDownload);
|
||||||
|
if (!itemInfo.CueSheetInfos.Empty())
|
||||||
|
{
|
||||||
|
localTracks.ForEach(localTrack =>
|
||||||
|
{
|
||||||
|
var cueSheetInfo = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path));
|
||||||
|
var cueSheet = cueSheetInfo?.CueSheet;
|
||||||
|
if (cueSheet != null)
|
||||||
|
{
|
||||||
|
localTrack.IsSingleFileRelease = cueSheet.IsSingleFileRelease;
|
||||||
|
localTrack.CueSheetFileEntry = cueSheetInfo.TryToGetFileEntryForMediaFile(localTrack.Path);
|
||||||
|
localTrack.Artist = idOverrides.Artist;
|
||||||
|
localTrack.Album = idOverrides.Album;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var localTracksByAlbums = localTracks.GroupBy(x => x.Album);
|
||||||
|
foreach (var localTracksByAlbum in localTracksByAlbums)
|
||||||
|
{
|
||||||
|
if (!localTracksByAlbum.All(x => x.IsSingleFileRelease == true))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
localTracks.ForEach(x =>
|
||||||
|
{
|
||||||
|
if (x.IsSingleFileRelease && localTracksByAlbum.Contains(x))
|
||||||
|
{
|
||||||
|
x.FileTrackInfo.DiscCount = localTracksByAlbum.Count();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var releases = _identificationService.Identify(localTracks, idOverrides, config);
|
var releases = _identificationService.Identify(localTracks, idOverrides, config);
|
||||||
|
|
||||||
|
@ -246,7 +285,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
{
|
{
|
||||||
ImportDecision<LocalTrack> decision = null;
|
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}")) :
|
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}"));
|
new ImportDecision<LocalTrack>(localTrack, new Rejection($"Couldn't parse album from: {localTrack.FileTrackInfo}"));
|
||||||
|
|
|
@ -15,6 +15,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
public QualityModel Quality { get; set; }
|
public QualityModel Quality { get; set; }
|
||||||
public string DownloadId { get; set; }
|
public string DownloadId { get; set; }
|
||||||
public bool DisableReleaseSwitching { get; set; }
|
public bool DisableReleaseSwitching { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public string CueSheetPath { get; set; }
|
||||||
|
|
||||||
public bool Equals(ManualImportFile other)
|
public bool Equals(ManualImportFile other)
|
||||||
{
|
{
|
||||||
|
|
|
@ -32,5 +32,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
public bool AdditionalFile { get; set; }
|
public bool AdditionalFile { get; set; }
|
||||||
public bool ReplaceExistingFiles { get; set; }
|
public bool ReplaceExistingFiles { get; set; }
|
||||||
public bool DisableReleaseSwitching { get; set; }
|
public bool DisableReleaseSwitching { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public string CueSheetPath { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
using NzbDrone.Core.CustomFormats;
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
|
@ -36,6 +37,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
private readonly IRootFolderService _rootFolderService;
|
private readonly IRootFolderService _rootFolderService;
|
||||||
private readonly IDiskScanService _diskScanService;
|
private readonly IDiskScanService _diskScanService;
|
||||||
private readonly IMakeImportDecision _importDecisionMaker;
|
private readonly IMakeImportDecision _importDecisionMaker;
|
||||||
|
private readonly ICueSheetService _cueSheetService;
|
||||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||||
private readonly IArtistService _artistService;
|
private readonly IArtistService _artistService;
|
||||||
private readonly IAlbumService _albumService;
|
private readonly IAlbumService _albumService;
|
||||||
|
@ -54,6 +56,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
IRootFolderService rootFolderService,
|
IRootFolderService rootFolderService,
|
||||||
IDiskScanService diskScanService,
|
IDiskScanService diskScanService,
|
||||||
IMakeImportDecision importDecisionMaker,
|
IMakeImportDecision importDecisionMaker,
|
||||||
|
ICueSheetService cueSheetService,
|
||||||
ICustomFormatCalculationService formatCalculator,
|
ICustomFormatCalculationService formatCalculator,
|
||||||
IArtistService artistService,
|
IArtistService artistService,
|
||||||
IAlbumService albumService,
|
IAlbumService albumService,
|
||||||
|
@ -72,6 +75,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
_rootFolderService = rootFolderService;
|
_rootFolderService = rootFolderService;
|
||||||
_diskScanService = diskScanService;
|
_diskScanService = diskScanService;
|
||||||
_importDecisionMaker = importDecisionMaker;
|
_importDecisionMaker = importDecisionMaker;
|
||||||
|
_cueSheetService = cueSheetService;
|
||||||
_formatCalculator = formatCalculator;
|
_formatCalculator = formatCalculator;
|
||||||
_artistService = artistService;
|
_artistService = artistService;
|
||||||
_albumService = albumService;
|
_albumService = albumService;
|
||||||
|
@ -149,16 +153,30 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
|
var audioFiles = _diskScanService.GetAudioFiles(folder).ToList();
|
||||||
|
var results = new List<ManualImportItem>();
|
||||||
|
|
||||||
var idOverrides = new IdentificationOverrides
|
var idOverrides = new IdentificationOverrides
|
||||||
{
|
{
|
||||||
Artist = artist
|
Artist = artist,
|
||||||
|
Album = null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
results.AddRange(ProcessFolder(downloadId, idOverrides, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ManualImportItem> ProcessFolder(string downloadId, IdentificationOverrides idOverrides, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List<IFileInfo> audioFiles, List<CueSheetInfo> cueSheetInfos = null)
|
||||||
|
{
|
||||||
|
idOverrides ??= new IdentificationOverrides();
|
||||||
var itemInfo = new ImportDecisionMakerInfo
|
var itemInfo = new ImportDecisionMakerInfo
|
||||||
{
|
{
|
||||||
DownloadClientItem = downloadClientItem,
|
DownloadClientItem = downloadClientItem,
|
||||||
ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(directoryInfo.Name)
|
ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle),
|
||||||
|
DetectCueFileEncoding = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var config = new ImportDecisionMakerConfig
|
var config = new ImportDecisionMakerConfig
|
||||||
{
|
{
|
||||||
Filter = filter,
|
Filter = filter,
|
||||||
|
@ -168,20 +186,26 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
AddNewArtists = false
|
AddNewArtists = false
|
||||||
};
|
};
|
||||||
|
|
||||||
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, idOverrides, itemInfo, config);
|
var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, null, itemInfo, config);
|
||||||
|
if (!audioFiles.Empty())
|
||||||
|
{
|
||||||
|
decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config));
|
||||||
|
}
|
||||||
|
|
||||||
// paths will be different for new and old files which is why we need to map separately
|
// 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,
|
f => f.FullName,
|
||||||
d => d.Item.Path,
|
d => d.Item.Path,
|
||||||
(f, d) => new { File = f, Decision = d },
|
(f, d) => new { File = f, Decision = d },
|
||||||
PathEqualityComparer.Instance);
|
PathEqualityComparer.Instance);
|
||||||
|
|
||||||
var newItems = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false));
|
var newItemsList = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false)).ToList();
|
||||||
|
|
||||||
var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision));
|
var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision));
|
||||||
var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false));
|
var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false));
|
||||||
|
|
||||||
return newItems.Concat(existingItems).ToList();
|
var itemsList = newItemsList.Concat(existingItems.ToList()).ToList();
|
||||||
|
return itemsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ManualImportItem> UpdateItems(List<ManualImportItem> items)
|
public List<ManualImportItem> UpdateItems(List<ManualImportItem> items)
|
||||||
|
@ -198,13 +222,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
|
|
||||||
var disableReleaseSwitching = group.First().DisableReleaseSwitching;
|
var disableReleaseSwitching = group.First().DisableReleaseSwitching;
|
||||||
|
|
||||||
var files = group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList();
|
|
||||||
var idOverride = new IdentificationOverrides
|
|
||||||
{
|
|
||||||
Artist = group.First().Artist,
|
|
||||||
Album = group.First().Album,
|
|
||||||
AlbumRelease = group.First().Release
|
|
||||||
};
|
|
||||||
var config = new ImportDecisionMakerConfig
|
var config = new ImportDecisionMakerConfig
|
||||||
{
|
{
|
||||||
Filter = FilterFilesType.None,
|
Filter = FilterFilesType.None,
|
||||||
|
@ -213,58 +230,97 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
IncludeExisting = !replaceExistingFiles,
|
IncludeExisting = !replaceExistingFiles,
|
||||||
AddNewArtists = false
|
AddNewArtists = false
|
||||||
};
|
};
|
||||||
var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, null, config);
|
|
||||||
|
|
||||||
var existingItems = group.Join(decisions,
|
var audioFiles = new List<IFileInfo>();
|
||||||
i => i.Path,
|
foreach (var item in group)
|
||||||
d => d.Item.Path,
|
|
||||||
(i, d) => new { Item = i, Decision = d },
|
|
||||||
PathEqualityComparer.Instance);
|
|
||||||
|
|
||||||
foreach (var pair in existingItems)
|
|
||||||
{
|
{
|
||||||
var item = pair.Item;
|
var file = _diskProvider.GetFileInfo(item.Path);
|
||||||
var decision = pair.Decision;
|
audioFiles.Add(file);
|
||||||
|
|
||||||
if (decision.Item.Artist != null)
|
if (item.CueSheetPath != null)
|
||||||
{
|
{
|
||||||
item.Artist = decision.Item.Artist;
|
var cueFile = _diskProvider.GetFileInfo(item.CueSheetPath);
|
||||||
|
audioFiles.Add(cueFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision.Item.Album != null)
|
|
||||||
{
|
|
||||||
item.Album = decision.Item.Album;
|
|
||||||
item.Release = decision.Item.Release;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decision.Item.Tracks.Any())
|
|
||||||
{
|
|
||||||
item.Tracks = decision.Item.Tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.Quality?.Quality == Quality.Unknown)
|
|
||||||
{
|
|
||||||
item.Quality = decision.Item.Quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.ReleaseGroup.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
item.ReleaseGroup = decision.Item.ReleaseGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Rejections = decision.Rejections;
|
|
||||||
item.Size = decision.Item.Size;
|
|
||||||
|
|
||||||
result.Add(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newDecisions = decisions.Except(existingItems.Select(x => x.Decision));
|
var itemInfo = new ImportDecisionMakerInfo();
|
||||||
result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching)));
|
var idOverride = new IdentificationOverrides
|
||||||
|
{
|
||||||
|
Artist = group.First().Artist,
|
||||||
|
Album = group.First().Album,
|
||||||
|
AlbumRelease = group.First().Release
|
||||||
|
};
|
||||||
|
|
||||||
|
itemInfo.DetectCueFileEncoding = true;
|
||||||
|
var decisions = _cueSheetService.GetImportDecisions(ref audioFiles, idOverride, itemInfo, config);
|
||||||
|
if (audioFiles.Count > 0)
|
||||||
|
{
|
||||||
|
decisions.AddRange(_importDecisionMaker.GetImportDecisions(audioFiles, idOverride, itemInfo, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decisions.Count > 0)
|
||||||
|
{
|
||||||
|
result.AddRange(UpdateItems(group, decisions, replaceExistingFiles, disableReleaseSwitching));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<ManualImportItem> UpdateItems(IGrouping<int?, ManualImportItem> group, List<ImportDecision<LocalTrack>> decisions, bool replaceExistingFiles, bool disableReleaseSwitching)
|
||||||
|
{
|
||||||
|
var result = new List<ManualImportItem>();
|
||||||
|
|
||||||
|
var existingItems = group.Join(decisions,
|
||||||
|
i => i.Path,
|
||||||
|
d => d.Item.Path,
|
||||||
|
(i, d) => new { Item = i, Decision = d },
|
||||||
|
PathEqualityComparer.Instance);
|
||||||
|
|
||||||
|
foreach (var pair in existingItems)
|
||||||
|
{
|
||||||
|
var item = pair.Item;
|
||||||
|
var decision = pair.Decision;
|
||||||
|
|
||||||
|
if (decision.Item.Artist != null)
|
||||||
|
{
|
||||||
|
item.Artist = decision.Item.Artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.Item.Album != null)
|
||||||
|
{
|
||||||
|
item.Album = decision.Item.Album;
|
||||||
|
item.Release = decision.Item.Release;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.Item.Tracks.Any())
|
||||||
|
{
|
||||||
|
item.Tracks = decision.Item.Tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Quality?.Quality == Quality.Unknown)
|
||||||
|
{
|
||||||
|
item.Quality = decision.Item.Quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.ReleaseGroup.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
item.ReleaseGroup = decision.Item.ReleaseGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Rejections = decision.Rejections;
|
||||||
|
item.Size = decision.Item.Size;
|
||||||
|
|
||||||
|
result.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDecisions = decisions.Except(existingItems.Select(x => x.Decision));
|
||||||
|
result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching)));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
|
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
|
||||||
{
|
{
|
||||||
var item = new ManualImportItem();
|
var item = new ManualImportItem();
|
||||||
|
@ -299,6 +355,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
item.AdditionalFile = decision.Item.AdditionalFile;
|
item.AdditionalFile = decision.Item.AdditionalFile;
|
||||||
item.ReplaceExistingFiles = replaceExistingFiles;
|
item.ReplaceExistingFiles = replaceExistingFiles;
|
||||||
item.DisableReleaseSwitching = disableReleaseSwitching;
|
item.DisableReleaseSwitching = disableReleaseSwitching;
|
||||||
|
item.IsSingleFileRelease = decision.Item.IsSingleFileRelease;
|
||||||
|
item.CueSheetPath = decision.Item.CueSheetPath;
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -346,9 +404,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
Quality = file.Quality,
|
Quality = file.Quality,
|
||||||
Artist = artist,
|
Artist = artist,
|
||||||
Album = album,
|
Album = album,
|
||||||
Release = release
|
Release = release,
|
||||||
|
IsSingleFileRelease = file.IsSingleFileRelease,
|
||||||
|
CueSheetPath = file.CueSheetPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (file.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
localTrack.Tracks.ForEach(x => x.IsSingleFileRelease = true);
|
||||||
|
}
|
||||||
|
|
||||||
var importDecision = new ImportDecision<LocalTrack>(localTrack);
|
var importDecision = new ImportDecision<LocalTrack>(localTrack);
|
||||||
if (_rootFolderService.GetBestRootFolder(artist.Path) == null)
|
if (_rootFolderService.GetBestRootFolder(artist.Path) == null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
_logger.Debug("Min quality of new files: {0}", newMinQuality);
|
_logger.Debug("Min quality of new files: {0}", newMinQuality);
|
||||||
|
|
||||||
// get minimum quality of existing release
|
// get minimum quality of existing release
|
||||||
var existingQualities = currentRelease.Tracks.Value.Where(x => x.TrackFileId != 0).Select(x => x.TrackFile.Value.Quality);
|
var existingQualities = currentRelease.Tracks.Value.Where(x => x.TrackFileId != 0 && x.TrackFile.Value != null).Select(x => x.TrackFile.Value.Quality);
|
||||||
if (existingQualities.Any())
|
if (existingQualities.Any())
|
||||||
{
|
{
|
||||||
var existingMinQuality = existingQualities.OrderBy(x => x, qualityComparer).First();
|
var existingMinQuality = existingQualities.OrderBy(x => x, qualityComparer).First();
|
||||||
|
|
|
@ -22,11 +22,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
{
|
{
|
||||||
double dist;
|
double dist;
|
||||||
string reasons;
|
string reasons;
|
||||||
|
if (item.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
// strict when a new download
|
// strict when a new download
|
||||||
if (item.NewDownload)
|
if (item.NewDownload)
|
||||||
{
|
{
|
||||||
dist = item.Distance.NormalizedDistance();
|
dist = item.Distance.NormalizedDistance();
|
||||||
|
|
||||||
reasons = item.Distance.Reasons;
|
reasons = item.Distance.Reasons;
|
||||||
if (dist > _albumThreshold)
|
if (dist > _albumThreshold)
|
||||||
{
|
{
|
||||||
|
|
|
@ -17,6 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
|
|
||||||
public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem)
|
public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem)
|
||||||
{
|
{
|
||||||
|
if (item.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
var dist = item.Distance.NormalizedDistance();
|
var dist = item.Distance.NormalizedDistance();
|
||||||
var reasons = item.Distance.Reasons;
|
var reasons = item.Distance.Reasons;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
|
|
||||||
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
|
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
|
||||||
{
|
{
|
||||||
|
if (item.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
var existingRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored);
|
var existingRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored);
|
||||||
var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile);
|
var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile);
|
||||||
if (item.AlbumRelease.Id != existingRelease.Id &&
|
if (item.AlbumRelease.Id != existingRelease.Id &&
|
||||||
|
|
|
@ -16,6 +16,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
|
|
||||||
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
|
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
|
||||||
{
|
{
|
||||||
|
if (item.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
if (item.NewDownload && item.TrackMapping.LocalExtra.Count > 0)
|
if (item.NewDownload && item.TrackMapping.LocalExtra.Count > 0)
|
||||||
{
|
{
|
||||||
_logger.Debug("This release has track files that have not been matched. Skipping {0}", item);
|
_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 Ratings Ratings { get; set; }
|
||||||
public int MediumNumber { get; set; }
|
public int MediumNumber { get; set; }
|
||||||
public int TrackFileId { get; set; }
|
public int TrackFileId { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
|
||||||
[MemberwiseEqualityIgnore]
|
[MemberwiseEqualityIgnore]
|
||||||
public bool HasFile => TrackFileId > 0;
|
public bool HasFile => TrackFileId > 0;
|
||||||
|
@ -73,6 +74,7 @@ namespace NzbDrone.Core.Music
|
||||||
Explicit = other.Explicit;
|
Explicit = other.Explicit;
|
||||||
Ratings = other.Ratings;
|
Ratings = other.Ratings;
|
||||||
MediumNumber = other.MediumNumber;
|
MediumNumber = other.MediumNumber;
|
||||||
|
IsSingleFileRelease = other.IsSingleFileRelease;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void UseDbFieldsFrom(Track other)
|
public override void UseDbFieldsFrom(Track other)
|
||||||
|
@ -81,6 +83,7 @@ namespace NzbDrone.Core.Music
|
||||||
AlbumReleaseId = other.AlbumReleaseId;
|
AlbumReleaseId = other.AlbumReleaseId;
|
||||||
ArtistMetadataId = other.ArtistMetadataId;
|
ArtistMetadataId = other.ArtistMetadataId;
|
||||||
TrackFileId = other.TrackFileId;
|
TrackFileId = other.TrackFileId;
|
||||||
|
IsSingleFileRelease = other.IsSingleFileRelease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,15 +119,23 @@ namespace NzbDrone.Core.Organizer
|
||||||
{
|
{
|
||||||
var splitPattern = splitPatterns[i];
|
var splitPattern = splitPatterns[i];
|
||||||
var tokenHandlers = new Dictionary<string, Func<TokenMatch, string>>(FileNameBuilderTokenEqualityComparer.Instance);
|
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);
|
AddArtistTokens(tokenHandlers, artist);
|
||||||
AddAlbumTokens(tokenHandlers, album);
|
AddAlbumTokens(tokenHandlers, album);
|
||||||
AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber));
|
AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber));
|
||||||
AddTrackTokens(tokenHandlers, tracks, artist);
|
if (!trackFile.IsSingleFileRelease)
|
||||||
AddTrackTitlePlaceholderTokens(tokenHandlers);
|
{
|
||||||
AddTrackFileTokens(tokenHandlers, trackFile);
|
AddTrackTokens(tokenHandlers, tracks, artist);
|
||||||
|
AddTrackTitlePlaceholderTokens(tokenHandlers);
|
||||||
|
AddTrackFileTokens(tokenHandlers, trackFile);
|
||||||
|
}
|
||||||
|
|
||||||
AddQualityTokens(tokenHandlers, artist, trackFile);
|
AddQualityTokens(tokenHandlers, artist, trackFile);
|
||||||
AddMediaInfoTokens(tokenHandlers, trackFile);
|
AddMediaInfoTokens(tokenHandlers, trackFile);
|
||||||
AddCustomFormats(tokenHandlers, artist, trackFile, customFormats);
|
AddCustomFormats(tokenHandlers, artist, trackFile, customFormats);
|
||||||
|
@ -141,9 +149,12 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
var maxTrackTitleLength = maxPathSegmentLength - GetLengthWithoutTrackTitle(component, namingConfig);
|
var maxTrackTitleLength = maxPathSegmentLength - GetLengthWithoutTrackTitle(component, namingConfig);
|
||||||
|
|
||||||
AddTrackTitleTokens(tokenHandlers, tracks, maxTrackTitleLength);
|
if (!trackFile.IsSingleFileRelease)
|
||||||
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
|
{
|
||||||
|
AddTrackTitleTokens(tokenHandlers, tracks, maxTrackTitleLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
component = ReplaceTokens(component, tokenHandlers, namingConfig).Trim();
|
||||||
component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString());
|
component = FileNameCleanupRegex.Replace(component, match => match.Captures[0].Value[0].ToString());
|
||||||
component = TrimSeparatorsRegex.Replace(component, string.Empty);
|
component = TrimSeparatorsRegex.Replace(component, string.Empty);
|
||||||
component = component.Replace("{ellipsis}", "...");
|
component = component.Replace("{ellipsis}", "...");
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
|
|
||||||
|
@ -61,6 +62,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
|
||||||
|
@ -68,10 +71,12 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
public TrackMapping()
|
public TrackMapping()
|
||||||
{
|
{
|
||||||
Mapping = new Dictionary<LocalTrack, Tuple<Track, Distance>>();
|
Mapping = new Dictionary<LocalTrack, Tuple<Track, Distance>>();
|
||||||
|
CuesheetTrackMapping = new Dictionary<CueSheet.TrackEntry, Tuple<Track, Distance>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<LocalTrack, Tuple<Track, Distance>> Mapping { get; set; }
|
public Dictionary<LocalTrack, Tuple<Track, Distance>> Mapping { get; set; }
|
||||||
public List<LocalTrack> LocalExtra { get; set; }
|
public List<LocalTrack> LocalExtra { get; set; }
|
||||||
public List<Track> MBExtra { get; set; }
|
public List<Track> MBExtra { get; set; }
|
||||||
|
public Dictionary<CueSheet.TrackEntry, Tuple<Track, Distance>> CuesheetTrackMapping { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -31,7 +32,9 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
public bool SceneSource { get; set; }
|
public bool SceneSource { get; set; }
|
||||||
public string ReleaseGroup { get; set; }
|
public string ReleaseGroup { get; set; }
|
||||||
public string SceneName { get; set; }
|
public string SceneName { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
public CueSheet.FileEntry CueSheetFileEntry { get; set; }
|
||||||
|
public string CueSheetPath { get; set; }
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Path;
|
return Path;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue