mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-24 07:15:20 -07:00
Add support to read cuesheet file and import a single-file release.
(cherry picked from commit 506e4415d613d3752605131d0f8b63fa448ee696)
This commit is contained in:
parent
30fc3fc70a
commit
31016bca8a
30 changed files with 311 additions and 56 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: 'isSingleFileRelease',
|
||||||
|
label: () => 'Is Single File Release',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'releaseGroup',
|
name: 'releaseGroup',
|
||||||
label: () => translate('ReleaseGroup'),
|
label: () => translate('ReleaseGroup'),
|
||||||
|
@ -435,6 +440,7 @@ class InteractiveImportModalContent extends Component {
|
||||||
allowArtistChange={allowArtistChange}
|
allowArtistChange={allowArtistChange}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onValidRowChange={this.onValidRowChange}
|
onValidRowChange={this.onValidRowChange}
|
||||||
|
isSingleFileRelease={item.isSingleFileRelease}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
album,
|
album,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
tracks,
|
tracks,
|
||||||
|
isSingleFileRelease,
|
||||||
quality,
|
quality,
|
||||||
disableReleaseSwitching
|
disableReleaseSwitching
|
||||||
} = item;
|
} = item;
|
||||||
|
@ -148,7 +149,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tracks || !tracks.length) {
|
if (!isSingleFileRelease && (!tracks || !tracks.length)) {
|
||||||
this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' });
|
this.setState({ interactiveImportErrorMessage: 'One or more tracks must be chosen for each selected file' });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -164,6 +165,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
trackIds: _.map(tracks, 'id'),
|
trackIds: _.map(tracks, 'id'),
|
||||||
|
isSingleFileRelease: item.isSingleFileRelease,
|
||||||
quality,
|
quality,
|
||||||
downloadId: this.props.downloadId,
|
downloadId: this.props.downloadId,
|
||||||
disableReleaseSwitching
|
disableReleaseSwitching
|
||||||
|
|
|
@ -64,6 +64,7 @@ class InteractiveImportRow extends Component {
|
||||||
artist,
|
artist,
|
||||||
album,
|
album,
|
||||||
tracks,
|
tracks,
|
||||||
|
isSingleFileRelease,
|
||||||
quality,
|
quality,
|
||||||
isSelected,
|
isSelected,
|
||||||
onValidRowChange
|
onValidRowChange
|
||||||
|
@ -82,7 +83,7 @@ class InteractiveImportRow extends Component {
|
||||||
const isValid = !!(
|
const isValid = !!(
|
||||||
artist &&
|
artist &&
|
||||||
album &&
|
album &&
|
||||||
tracks.length &&
|
(isSingleFileRelease || tracks.length) &&
|
||||||
quality
|
quality
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -167,6 +168,7 @@ class InteractiveImportRow extends Component {
|
||||||
album,
|
album,
|
||||||
albumReleaseId,
|
albumReleaseId,
|
||||||
tracks,
|
tracks,
|
||||||
|
isSingleFileRelease,
|
||||||
quality,
|
quality,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
size,
|
size,
|
||||||
|
@ -257,7 +259,7 @@ class InteractiveImportRow extends Component {
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
|
||||||
<TableRowCellButton
|
<TableRowCellButton
|
||||||
isDisabled={!artist || !album}
|
isDisabled={!artist || !album || isSingleFileRelease}
|
||||||
title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined}
|
title={artist && album ? translate('ArtistAlbumClickToChangeTrack') : undefined}
|
||||||
onPress={this.onSelectTrackPress}
|
onPress={this.onSelectTrackPress}
|
||||||
>
|
>
|
||||||
|
@ -265,10 +267,20 @@ class InteractiveImportRow extends Component {
|
||||||
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
|
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
|
!isSingleFileRelease && showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
|
||||||
}
|
}
|
||||||
|
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
id={id}
|
||||||
|
title={'Is Single File Release'}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isSingleFileRelease ? 'Yes' : 'No'
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCellButton
|
<TableRowCellButton
|
||||||
title={translate('ClickToChangeReleaseGroup')}
|
title={translate('ClickToChangeReleaseGroup')}
|
||||||
onPress={this.onSelectReleaseGroupPress}
|
onPress={this.onSelectReleaseGroupPress}
|
||||||
|
@ -408,7 +420,8 @@ 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,
|
||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
quality: PropTypes.object,
|
quality: PropTypes.object,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
|
|
|
@ -206,6 +206,7 @@ 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,
|
||||||
quality: item.quality,
|
quality: item.quality,
|
||||||
releaseGroup: item.releaseGroup,
|
releaseGroup: item.releaseGroup,
|
||||||
downloadId: item.downloadId,
|
downloadId: item.downloadId,
|
||||||
|
|
|
@ -83,7 +83,8 @@ 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ 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 static class ManualImportResourceMapper
|
public static class ManualImportResourceMapper
|
||||||
|
@ -52,6 +53,7 @@ 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,
|
||||||
|
|
||||||
// QualityWeight
|
// QualityWeight
|
||||||
DownloadId = model.DownloadId,
|
DownloadId = model.DownloadId,
|
||||||
|
|
|
@ -21,6 +21,7 @@ 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 IEnumerable<Rejection> Rejections { get; set; }
|
public IEnumerable<Rejection> Rejections { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ namespace Lidarr.Api.V1.Tracks
|
||||||
|
|
||||||
public ArtistResource Artist { get; set; }
|
public ArtistResource Artist { get; set; }
|
||||||
public Ratings Ratings { get; set; }
|
public Ratings Ratings { get; set; }
|
||||||
|
public bool IsSingleFileRelease { get; set; }
|
||||||
|
|
||||||
// Hiding this so people don't think its usable (only used to set the initial state)
|
// Hiding this so people don't think its usable (only used to set the initial state)
|
||||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
@ -58,6 +59,7 @@ namespace Lidarr.Api.V1.Tracks
|
||||||
MediumNumber = model.MediumNumber,
|
MediumNumber = model.MediumNumber,
|
||||||
HasFile = model.HasFile,
|
HasFile = model.HasFile,
|
||||||
Ratings = model.Ratings,
|
Ratings = model.Ratings,
|
||||||
|
IsSingleFileRelease = model.IsSingleFileRelease
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs
Normal file
14
src/NzbDrone.Core/Datastore/Migration/073_add_flac_cue.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(073)]
|
||||||
|
public class add_flac_cue : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("TrackFiles").AddColumn("IsSingleFileRelease").AsBoolean().WithDefaultValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,8 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
{ ".ape", Quality.APE },
|
{ ".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; }
|
||||||
|
|
|
@ -65,14 +65,25 @@ 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 (tracks.Count == 1 && tracks[0].IsSingleFileRelease)
|
||||||
{
|
{
|
||||||
foreach (var pattern in Patterns(charSep.Item1, charSep.Item2))
|
tracks[0].FileTrackInfo.ArtistTitle = tracks[0].Artist.Name;
|
||||||
|
tracks[0].FileTrackInfo.AlbumTitle = tracks[0].Album.Title;
|
||||||
|
|
||||||
|
// TODO this is too bold, the release year is not the one from the .cue file
|
||||||
|
tracks[0].FileTrackInfo.Year = (uint)tracks[0].Album.ReleaseDate.Value.Year;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var charSep in CharsAndSeps)
|
||||||
{
|
{
|
||||||
var matches = AllMatches(tracks, pattern);
|
foreach (var pattern in Patterns(charSep.Item1, charSep.Item2))
|
||||||
if (matches != null)
|
|
||||||
{
|
{
|
||||||
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.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
|
||||||
|
.OrderBy(x => x.ReleaseDate)
|
||||||
|
.ToList(), includeExisting);
|
||||||
|
}
|
||||||
|
|
||||||
// sort candidate releases by closest track count so that we stand a chance of
|
// 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)
|
||||||
|
|
|
@ -118,13 +118,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.Count == 1 && localTracks[0].IsSingleFileRelease) ? 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,29 +179,36 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracks
|
// tracks
|
||||||
foreach (var pair in mapping.Mapping)
|
if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease)
|
||||||
{
|
{
|
||||||
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
dist.Add("tracks", 0);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
|
||||||
|
|
||||||
// missing tracks
|
|
||||||
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
|
||||||
{
|
{
|
||||||
dist.Add("missing_tracks", 1.0);
|
foreach (var pair in mapping.Mapping)
|
||||||
|
{
|
||||||
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
||||||
|
|
||||||
|
// missing tracks
|
||||||
|
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
||||||
|
{
|
||||||
|
dist.Add("missing_tracks", 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
|
||||||
|
|
||||||
|
// unmatched tracks
|
||||||
|
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
|
||||||
|
{
|
||||||
|
dist.Add("unmatched_tracks", 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
|
|
||||||
|
|
||||||
// unmatched tracks
|
|
||||||
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
|
|
||||||
{
|
|
||||||
dist.Add("unmatched_tracks", 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
|
|
||||||
|
|
||||||
return dist;
|
return dist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -154,6 +154,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
|
|
||||||
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
|
private bool ShouldFingerprint(LocalAlbumRelease localAlbumRelease)
|
||||||
{
|
{
|
||||||
|
if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
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())
|
||||||
|
@ -335,6 +340,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
localAlbumRelease.AlbumRelease = release;
|
localAlbumRelease.AlbumRelease = release;
|
||||||
localAlbumRelease.ExistingTracks = extraTracks;
|
localAlbumRelease.ExistingTracks = extraTracks;
|
||||||
localAlbumRelease.TrackMapping = mapping;
|
localAlbumRelease.TrackMapping = mapping;
|
||||||
|
if (localAlbumRelease.LocalTracks.Count == 1 && localAlbumRelease.LocalTracks[0].IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
localAlbumRelease.LocalTracks[0].Tracks = release.Tracks;
|
||||||
|
localAlbumRelease.LocalTracks[0].Tracks.ForEach(x => x.IsSingleFileRelease = true);
|
||||||
|
}
|
||||||
|
|
||||||
if (currDistance == 0.0)
|
if (currDistance == 0.0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
@ -348,6 +359,14 @@ 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();
|
||||||
|
if (localTracks.Count == 1 && localTracks[0].IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
result.IsSingleFileRelease = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
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 +383,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]));
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -32,6 +33,7 @@ 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 bool IsSingleFileRelease { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImportDecisionMakerConfig
|
public class ImportDecisionMakerConfig
|
||||||
|
@ -149,6 +151,12 @@ 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);
|
||||||
|
localTracks.ForEach(x => x.IsSingleFileRelease = itemInfo.IsSingleFileRelease);
|
||||||
|
if (itemInfo.IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
localTracks.ForEach(x => x.Artist = idOverrides.Artist);
|
||||||
|
localTracks.ForEach(x => x.Album = idOverrides.Album);
|
||||||
|
}
|
||||||
|
|
||||||
var releases = _identificationService.Identify(localTracks, idOverrides, config);
|
var releases = _identificationService.Identify(localTracks, idOverrides, config);
|
||||||
|
|
||||||
|
@ -246,7 +254,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,7 @@ 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 bool Equals(ManualImportFile other)
|
public bool Equals(ManualImportFile other)
|
||||||
{
|
{
|
||||||
|
|
|
@ -32,5 +32,6 @@ 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Crypto;
|
using NzbDrone.Common.Crypto;
|
||||||
|
@ -132,6 +134,33 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
return ProcessFolder(path, downloadId, artist, filter, replaceExistingFiles);
|
return ProcessFolder(path, downloadId, artist, filter, replaceExistingFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string> ReadFieldFromCuesheet(string[] lines, string fieldName)
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
var candidates = lines.Where(l => l.StartsWith(fieldName)).ToList();
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
var matches = Regex.Matches(candidate, "\"(.*?)\"");
|
||||||
|
var result = matches.ToList()[0].Groups[1].Value;
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadOptionalFieldFromCuesheet(string[] lines, string fieldName)
|
||||||
|
{
|
||||||
|
var results = lines.Where(l => l.StartsWith(fieldName));
|
||||||
|
if (results.Any())
|
||||||
|
{
|
||||||
|
var matches = Regex.Matches(results.ToList()[0], fieldName + " (.+)");
|
||||||
|
var result = matches.ToList()[0].Groups[1].Value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, Artist artist, FilterFilesType filter, bool replaceExistingFiles)
|
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, Artist artist, FilterFilesType filter, bool replaceExistingFiles)
|
||||||
{
|
{
|
||||||
DownloadClientItem downloadClientItem = null;
|
DownloadClientItem downloadClientItem = null;
|
||||||
|
@ -149,15 +178,91 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
|
var audioFiles = _diskScanService.GetAudioFiles(folder).ToList();
|
||||||
|
var results = new List<ManualImportItem>();
|
||||||
|
|
||||||
|
// Split cue and non-cue files
|
||||||
|
var cueFiles = audioFiles.Where(x => x.Extension.Equals(".cue")).ToList();
|
||||||
|
audioFiles.RemoveAll(l => cueFiles.Contains(l));
|
||||||
|
foreach (var cueFile in cueFiles)
|
||||||
|
{
|
||||||
|
using (var fs = cueFile.OpenRead())
|
||||||
|
{
|
||||||
|
var bytes = new byte[cueFile.Length];
|
||||||
|
var encoding = new UTF8Encoding(true);
|
||||||
|
string content;
|
||||||
|
while (fs.Read(bytes, 0, bytes.Length) > 0)
|
||||||
|
{
|
||||||
|
content = encoding.GetString(bytes);
|
||||||
|
var lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
|
||||||
|
|
||||||
|
// Single-file cue means it's an unsplit image
|
||||||
|
var fileNames = ReadFieldFromCuesheet(lines, "FILE");
|
||||||
|
if (fileNames.Empty() || fileNames.Count > 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = fileNames[0];
|
||||||
|
if (!fileName.Empty())
|
||||||
|
{
|
||||||
|
Artist artistFromCue = null;
|
||||||
|
var artistNames = ReadFieldFromCuesheet(lines, "PERFORMER");
|
||||||
|
if (artistNames.Count > 0)
|
||||||
|
{
|
||||||
|
artistFromCue = _parsingService.GetArtist(artistNames[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
string albumTitle = null;
|
||||||
|
var albumTitles = ReadFieldFromCuesheet(lines, "TITLE");
|
||||||
|
if (artistNames.Count > 0)
|
||||||
|
{
|
||||||
|
albumTitle = albumTitles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var date = ReadOptionalFieldFromCuesheet(lines, "REM DATE");
|
||||||
|
var audioFile = audioFiles.Find(x => x.Name == fileName && x.DirectoryName == cueFile.DirectoryName);
|
||||||
|
var parsedAlbumInfo = new ParsedAlbumInfo
|
||||||
|
{
|
||||||
|
AlbumTitle = albumTitle,
|
||||||
|
ArtistName = artistFromCue.Name,
|
||||||
|
ReleaseDate = date,
|
||||||
|
};
|
||||||
|
var albumsFromCue = _parsingService.GetAlbums(parsedAlbumInfo, artistFromCue);
|
||||||
|
if (albumsFromCue == null || albumsFromCue.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempAudioFiles = new List<IFileInfo>
|
||||||
|
{
|
||||||
|
audioFile
|
||||||
|
};
|
||||||
|
|
||||||
|
results.AddRange(ProcessFolder(downloadId, artistFromCue, albumsFromCue[0], filter, replaceExistingFiles, downloadClientItem, albumTitle, tempAudioFiles, true));
|
||||||
|
audioFiles.Remove(audioFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.AddRange(ProcessFolder(downloadId, artist, null, filter, replaceExistingFiles, downloadClientItem, directoryInfo.Name, audioFiles, false));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ManualImportItem> ProcessFolder(string downloadId, Artist overrideArtist, Album overrideAlbum, FilterFilesType filter, bool replaceExistingFiles, DownloadClientItem downloadClientItem, string albumTitle, List<IFileInfo> audioFiles, bool isSingleFileRelease)
|
||||||
|
{
|
||||||
var idOverrides = new IdentificationOverrides
|
var idOverrides = new IdentificationOverrides
|
||||||
{
|
{
|
||||||
Artist = artist
|
Artist = overrideArtist,
|
||||||
|
Album = overrideAlbum
|
||||||
};
|
};
|
||||||
var itemInfo = new ImportDecisionMakerInfo
|
var itemInfo = new ImportDecisionMakerInfo
|
||||||
{
|
{
|
||||||
DownloadClientItem = downloadClientItem,
|
DownloadClientItem = downloadClientItem,
|
||||||
ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(directoryInfo.Name)
|
ParsedAlbumInfo = Parser.Parser.ParseAlbumTitle(albumTitle),
|
||||||
|
IsSingleFileRelease = isSingleFileRelease
|
||||||
};
|
};
|
||||||
var config = new ImportDecisionMakerConfig
|
var config = new ImportDecisionMakerConfig
|
||||||
{
|
{
|
||||||
|
@ -168,10 +273,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
AddNewArtists = false
|
AddNewArtists = false
|
||||||
};
|
};
|
||||||
|
|
||||||
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, idOverrides, itemInfo, config);
|
var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, itemInfo, config);
|
||||||
|
|
||||||
// paths will be different for new and old files which is why we need to map separately
|
// 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 },
|
||||||
|
@ -299,6 +404,7 @@ 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;
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -346,9 +452,15 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -22,11 +22,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
|
||||||
{
|
{
|
||||||
double dist;
|
double dist;
|
||||||
string reasons;
|
string reasons;
|
||||||
|
if (item.LocalTracks.Count == 1 && item.LocalTracks[0].IsSingleFileRelease)
|
||||||
|
{
|
||||||
|
_logger.Debug($"Accepting single file release {item}: {item.Distance.Reasons}");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
// strict when a new download
|
// 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.LocalTracks.Count == 1 && item.LocalTracks[0].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.LocalTracks.Count == 1 && item.LocalTracks[0].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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,12 +105,15 @@ namespace NzbDrone.Core.Organizer
|
||||||
|
|
||||||
var pattern = namingConfig.StandardTrackFormat;
|
var pattern = namingConfig.StandardTrackFormat;
|
||||||
|
|
||||||
if (tracks.First().AlbumRelease.Value.Media.Count > 1)
|
if (!trackFile.IsSingleFileRelease)
|
||||||
{
|
{
|
||||||
pattern = namingConfig.MultiDiscTrackFormat;
|
if (tracks.First().AlbumRelease.Value.Media.Count > 1)
|
||||||
}
|
{
|
||||||
|
pattern = namingConfig.MultiDiscTrackFormat;
|
||||||
|
}
|
||||||
|
|
||||||
tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList();
|
tracks = tracks.OrderBy(e => e.AlbumReleaseId).ThenBy(e => e.TrackNumber).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
var splitPatterns = pattern.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
var splitPatterns = pattern.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var components = new List<string>();
|
var components = new List<string>();
|
||||||
|
@ -119,15 +122,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));
|
if (!trackFile.IsSingleFileRelease)
|
||||||
AddTrackTokens(tokenHandlers, tracks, artist);
|
{
|
||||||
AddTrackTitlePlaceholderTokens(tokenHandlers);
|
AddMediumTokens(tokenHandlers, tracks.First().AlbumRelease.Value.Media.SingleOrDefault(m => m.Number == tracks.First().MediumNumber));
|
||||||
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 +152,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}", "...");
|
||||||
|
|
|
@ -73,5 +73,6 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
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 bool IsSingleFileRelease { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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 override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Path;
|
return Path;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue