using NLog; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Parser; using NzbDrone.Common.Extensions; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace NzbDrone.Core.Music { public interface ITrackService { Track GetTrack(int id); List GetTracks(IEnumerable ids); Track FindTrack(int artistId, int albumId, int mediumNumber, int trackNumber); Track FindTrackByTitle(int artistId, int albumId, int mediumNumber, int trackNumber, string releaseTitle); Track FindTrackByTitleInexact(int artistId, int albumId, int mediumNumber, int trackNumber, string releaseTitle); List GetTracksByArtist(int artistId); List GetTracksByAlbum(int albumId); //List GetTracksByAlbumTitle(string artistId, string albumTitle); List TracksWithFiles(int artistId); //PagingSpec TracksWithoutFiles(PagingSpec pagingSpec); List GetTracksByFileId(int trackFileId); void UpdateTrack(Track track); void SetTrackMonitored(int trackId, bool monitored); void UpdateTracks(List tracks); void InsertMany(List tracks); void UpdateMany(List tracks); void DeleteMany(List tracks); void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored); } public class TrackService : ITrackService, IHandleAsync, IHandleAsync, IHandle, IHandle { private readonly ITrackRepository _trackRepository; private readonly IConfigService _configService; private readonly Logger _logger; public TrackService(ITrackRepository trackRepository, IConfigService configService, Logger logger) { _trackRepository = trackRepository; _configService = configService; _logger = logger; } public Track GetTrack(int id) { return _trackRepository.Get(id); } public List GetTracks(IEnumerable ids) { return _trackRepository.Get(ids).ToList(); } public Track FindTrack(int artistId, int albumId, int mediumNumber, int trackNumber) { return _trackRepository.Find(artistId, albumId, mediumNumber, trackNumber); } public List GetTracksByArtist(int artistId) { _logger.Debug("Getting Tracks for ArtistId {0}", artistId); return _trackRepository.GetTracks(artistId).ToList(); } public List GetTracksByAlbum(int albumId) { return _trackRepository.GetTracksByAlbum(albumId); } public Track FindTrackByTitle(int artistId, int albumId, int mediumNumber, int trackNumber, string releaseTitle) { // TODO: can replace this search mechanism with something smarter/faster/better var normalizedReleaseTitle = releaseTitle.NormalizeTrackTitle().Replace(".", " "); var tracks = _trackRepository.GetTracksByMedium(albumId, mediumNumber); var matches = tracks.Where(t => (trackNumber == 0 || t.AbsoluteTrackNumber == trackNumber) && t.Title.Length > 0 && (normalizedReleaseTitle.Contains(t.Title.NormalizeTrackTitle()) || t.Title.NormalizeTrackTitle().Contains(normalizedReleaseTitle))); return matches.Count() > 1 ? null : matches.SingleOrDefault(); } public Track FindTrackByTitleInexact(int artistId, int albumId, int mediumNumber, int trackNumber, string title) { var normalizedTitle = title.NormalizeTrackTitle().Replace(".", " "); var tracks = _trackRepository.GetTracksByMedium(albumId, mediumNumber); Func< Func, string, Tuple, string>> tc = Tuple.Create; var scoringFunctions = new List, string>> { tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyMatch(t), normalizedTitle), tc((a, t) => a.Title.NormalizeTrackTitle().FuzzyContains(t), normalizedTitle), tc((a, t) => t.FuzzyContains(a.Title.NormalizeTrackTitle()), normalizedTitle) }; foreach (var func in scoringFunctions) { var track = FindByStringInexact(tracks, func.Item1, func.Item2, trackNumber); if (track != null) { return track; } } return null; } private Track FindByStringInexact(List tracks, Func scoreFunction, string title, int trackNumber) { const double fuzzThreshold = 0.7; const double fuzzGap = 0.2; var sortedTracks = tracks.Select(s => new { MatchProb = scoreFunction(s, title), Track = s }) .ToList() .OrderByDescending(s => s.MatchProb) .ToList(); if (!sortedTracks.Any()) { return null; } _logger.Trace("\nFuzzy track match on '{0:D2} - {1}':\n{2}", trackNumber, title, string.Join("\n", sortedTracks.Select(x => $"{x.Track.AbsoluteTrackNumber:D2} - {x.Track.Title}: {x.MatchProb}"))); if (sortedTracks[0].MatchProb > fuzzThreshold && (sortedTracks.Count == 1 || sortedTracks[0].MatchProb - sortedTracks[1].MatchProb > fuzzGap) && (trackNumber == 0 || sortedTracks[0].Track.AbsoluteTrackNumber == trackNumber || sortedTracks[0].Track.AbsoluteTrackNumber + tracks.Count(t => t.MediumNumber < sortedTracks[0].Track.MediumNumber) == trackNumber)) { return sortedTracks[0].Track; } return null; } public List TracksWithFiles(int artistId) { return _trackRepository.TracksWithFiles(artistId); } public PagingSpec TracksWithoutFiles(PagingSpec pagingSpec) { var episodeResult = _trackRepository.TracksWithoutFiles(pagingSpec); return episodeResult; } public List GetTracksByFileId(int trackFileId) { return _trackRepository.GetTracksByFileId(trackFileId); } public void UpdateTrack(Track track) { _trackRepository.Update(track); } public void SetTrackMonitored(int trackId, bool monitored) { var track = _trackRepository.Get(trackId); _trackRepository.SetMonitoredFlat(track, monitored); _logger.Debug("Monitored flag for Track:{0} was set to {1}", trackId, monitored); } public void SetTrackMonitoredByAlbum(int artistId, int albumId, bool monitored) { _trackRepository.SetMonitoredByAlbum(artistId, albumId, monitored); } public void UpdateTracks(List tracks) { _trackRepository.UpdateMany(tracks); } public void InsertMany(List tracks) { _trackRepository.InsertMany(tracks); } public void UpdateMany(List tracks) { _trackRepository.UpdateMany(tracks); } public void DeleteMany(List tracks) { _trackRepository.DeleteMany(tracks); } public void HandleAsync(ArtistDeletedEvent message) { var tracks = GetTracksByArtist(message.Artist.Id); _trackRepository.DeleteMany(tracks); } public void HandleAsync(AlbumDeletedEvent message) { var tracks = GetTracksByAlbum(message.Album.Id); _trackRepository.DeleteMany(tracks); } public void Handle(TrackFileDeletedEvent message) { foreach (var track in GetTracksByFileId(message.TrackFile.Id)) { _logger.Debug("Detaching track {0} from file.", track.Id); track.TrackFileId = 0; if (message.Reason != DeleteMediaFileReason.Upgrade && _configService.AutoUnmonitorPreviouslyDownloadedTracks) { track.Monitored = false; } UpdateTrack(track); } } public void Handle(TrackFileAddedEvent message) { foreach (var track in message.TrackFile.Tracks.Value) { _trackRepository.SetFileId(track.Id, message.TrackFile.Id); _logger.Debug("Linking [{0}] > [{1}]", message.TrackFile.RelativePath, track); } } } }