mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-21 05:53:33 -07:00
Implement the track distance calculation for cue sheet tracks.
Implement the diacritics and punctuation marks sanitation feature for the cue sheet track mapping. # Conflicts: # src/NzbDrone.Core/Lidarr.Core.csproj (cherry picked from commit 27622cebe9433b27bb0e7dfaf3720e2bd284e513)
This commit is contained in:
parent
853fdf7ff2
commit
26678e79fa
7 changed files with 181 additions and 25 deletions
|
@ -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" />
|
||||||
|
|
|
@ -3,7 +3,9 @@ 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 System.Text.RegularExpressions;
|
||||||
|
using Diacritics.Extensions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Core.MediaFiles.TrackImport;
|
using NzbDrone.Core.MediaFiles.TrackImport;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
|
@ -19,6 +21,15 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
public IdentificationOverrides IdOverrides { get; set; }
|
public IdentificationOverrides IdOverrides { get; set; }
|
||||||
public CueSheet CueSheet { get; set; }
|
public CueSheet CueSheet { get; set; }
|
||||||
public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && CueSheet.Files.Any(x => Path.GetFileName(path) == x.Name);
|
public bool IsForMediaFile(string path) => CueSheet != null && CueSheet.Files.Count > 0 && 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
|
public interface ICueSheetService
|
||||||
|
@ -50,6 +61,39 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
_logger = logger;
|
_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)
|
public List<ImportDecision<LocalTrack>> GetImportDecisions(ref List<IFileInfo> mediaFileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config)
|
||||||
{
|
{
|
||||||
var decisions = new List<ImportDecision<LocalTrack>>();
|
var decisions = new List<ImportDecision<LocalTrack>>();
|
||||||
|
@ -119,7 +163,6 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var addedTracks = new List<Track>();
|
|
||||||
decisions.ForEach(decision =>
|
decisions.ForEach(decision =>
|
||||||
{
|
{
|
||||||
if (!decision.Item.IsSingleFileRelease)
|
if (!decision.Item.IsSingleFileRelease)
|
||||||
|
@ -156,14 +199,53 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO diacritics could cause false positives here
|
var replacer = new PunctuationReplacer();
|
||||||
decision.Item.Tracks = tracksFromRelease.Where(trackFromRelease => !addedTracks.Contains(trackFromRelease) && tracksFromCueSheet.Any(trackFromCueSheet => string.Equals(trackFromCueSheet.Title, trackFromRelease.Title, StringComparison.OrdinalIgnoreCase))).ToList();
|
var i = 0;
|
||||||
addedTracks.AddRange(decision.Item.Tracks);
|
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;
|
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)
|
private CueSheet LoadCueSheet(IFileInfo fileInfo)
|
||||||
{
|
{
|
||||||
using (var fs = fileInfo.OpenRead())
|
using (var fs = fileInfo.OpenRead())
|
||||||
|
@ -337,7 +419,7 @@ namespace NzbDrone.Core.MediaFiles
|
||||||
}
|
}
|
||||||
else if (performers.Count > 1)
|
else if (performers.Count > 1)
|
||||||
{
|
{
|
||||||
return _parsingService.GetArtist("Various Artist");
|
return _parsingService.GetArtist("various artists");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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();
|
||||||
|
@ -179,9 +206,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
}
|
}
|
||||||
|
|
||||||
// tracks
|
// tracks
|
||||||
if (localTracks.All(x => x.IsSingleFileRelease == true))
|
if (mapping.CuesheetTrackMapping.Count != 0)
|
||||||
{
|
{
|
||||||
dist.Add("tracks", 0);
|
foreach (var pair in mapping.CuesheetTrackMapping)
|
||||||
|
{
|
||||||
|
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -192,14 +224,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
|
|
||||||
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
|
||||||
|
|
||||||
// missing tracks
|
|
||||||
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
|
|
||||||
{
|
|
||||||
dist.Add("missing_tracks", 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
|
|
||||||
|
|
||||||
// unmatched tracks
|
// unmatched tracks
|
||||||
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
|
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
|
||||||
{
|
{
|
||||||
|
@ -209,6 +233,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
|
||||||
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
|
Logger.Trace("after unmatched tracks: {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());
|
||||||
|
|
||||||
return dist;
|
return dist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -323,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();
|
||||||
|
|
||||||
|
@ -356,12 +357,6 @@ 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 result = new TrackMapping();
|
||||||
result.IsSingleFileRelease = localTracks.All(x => x.IsSingleFileRelease == true);
|
|
||||||
if (result.IsSingleFileRelease)
|
|
||||||
{
|
|
||||||
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];
|
||||||
|
|
||||||
|
@ -392,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,11 +159,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
|
||||||
{
|
{
|
||||||
localTracks.ForEach(localTrack =>
|
localTracks.ForEach(localTrack =>
|
||||||
{
|
{
|
||||||
var cueSheetFindResult = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path));
|
var cueSheetInfo = itemInfo.CueSheetInfos.Find(x => x.IsForMediaFile(localTrack.Path));
|
||||||
var cueSheet = cueSheetFindResult?.CueSheet;
|
var cueSheet = cueSheetInfo?.CueSheet;
|
||||||
if (cueSheet != null)
|
if (cueSheet != null)
|
||||||
{
|
{
|
||||||
localTrack.IsSingleFileRelease = cueSheet.IsSingleFileRelease;
|
localTrack.IsSingleFileRelease = cueSheet.IsSingleFileRelease;
|
||||||
|
localTrack.CueSheetFileEntry = cueSheetInfo.TryToGetFileEntryForMediaFile(localTrack.Path);
|
||||||
localTrack.Artist = idOverrides.Artist;
|
localTrack.Artist = idOverrides.Artist;
|
||||||
localTrack.Album = idOverrides.Album;
|
localTrack.Album = idOverrides.Album;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
@ -70,11 +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 bool IsSingleFileRelease { 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;
|
||||||
|
@ -32,6 +33,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
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 bool IsSingleFileRelease { get; set; }
|
||||||
|
public CueSheet.FileEntry CueSheetFileEntry { get; set; }
|
||||||
public string CueSheetPath { get; set; }
|
public string CueSheetPath { get; set; }
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue