using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Music { public interface IAlbumService { Album GetAlbum(int albumId); List GetAlbums(IEnumerable albumIds); List GetAlbumsByArtist(int artistId); List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds); List GetAlbumsByArtistMetadataId(int artistMetadataId); List GetAlbumsForRefresh(int artistMetadataId, List foreignIds); Album AddAlbum(Album newAlbum, bool doRefresh); Album FindById(string foreignId); Album FindByTitle(int artistMetadataId, string title); Album FindByTitleInexact(int artistMetadataId, string title); List GetCandidates(int artistMetadataId, string title); void DeleteAlbum(int albumId, bool deleteFiles, bool addImportListExclusion = false); List GetAllAlbums(); Album UpdateAlbum(Album album); void SetAlbumMonitored(int albumId, bool monitored); void SetMonitored(IEnumerable ids, bool monitored); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); List ArtistAlbumsBetweenDates(Artist artist, DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List albums); void UpdateMany(List albums); void DeleteMany(List albums); void SetAddOptions(IEnumerable albums); Album FindAlbumByRelease(string albumReleaseId); Album FindAlbumByTrackId(int trackId); List GetArtistAlbumsWithFiles(Artist artist); } public class AlbumService : IAlbumService, IHandle { private readonly IAlbumRepository _albumRepository; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public AlbumService(IAlbumRepository albumRepository, IEventAggregator eventAggregator, Logger logger) { _albumRepository = albumRepository; _eventAggregator = eventAggregator; _logger = logger; } public Album AddAlbum(Album newAlbum, bool doRefresh) { _albumRepository.Insert(newAlbum); _eventAggregator.PublishEvent(new AlbumAddedEvent(GetAlbum(newAlbum.Id), doRefresh)); return newAlbum; } public void DeleteAlbum(int albumId, bool deleteFiles, bool addImportListExclusion = false) { var album = _albumRepository.Get(albumId); album.Artist.LazyLoad(); _albumRepository.Delete(albumId); _eventAggregator.PublishEvent(new AlbumDeletedEvent(album, deleteFiles, addImportListExclusion)); } public Album FindById(string foreignId) { return _albumRepository.FindById(foreignId); } public Album FindByTitle(int artistMetadataId, string title) { return _albumRepository.FindByTitle(artistMetadataId, title); } private List, string>> AlbumScoringFunctions(string title, string cleanTitle) { Func, string, Tuple, string>> tc = Tuple.Create; var scoringFunctions = new List, string>> { tc((a, t) => a.CleanTitle.FuzzyMatch(t), cleanTitle), tc((a, t) => a.Title.FuzzyMatch(t), title), tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveBracketsAndContents().CleanArtistName()), tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveAfterDash().CleanArtistName()), tc((a, t) => a.CleanTitle.FuzzyMatch(t), title.RemoveBracketsAndContents().RemoveAfterDash().CleanArtistName()), tc((a, t) => t.FuzzyContains(a.CleanTitle), cleanTitle), tc((a, t) => t.FuzzyContains(a.Title), title) }; return scoringFunctions; } public Album FindByTitleInexact(int artistMetadataId, string title) { var albums = GetAlbumsByArtistMetadataId(artistMetadataId); foreach (var func in AlbumScoringFunctions(title, title.CleanArtistName())) { var results = FindByStringInexact(albums, func.Item1, func.Item2); if (results.Count == 1) { return results[0]; } } return null; } public List GetCandidates(int artistMetadataId, string title) { var albums = GetAlbumsByArtistMetadataId(artistMetadataId); var output = new List(); foreach (var func in AlbumScoringFunctions(title, title.CleanArtistName())) { output.AddRange(FindByStringInexact(albums, func.Item1, func.Item2)); } return output.DistinctBy(x => x.Id).ToList(); } private List FindByStringInexact(List albums, Func scoreFunction, string title) { const double fuzzThreshold = 0.7; const double fuzzGap = 0.4; var sortedAlbums = albums.Select(s => new { MatchProb = scoreFunction(s, title), Album = s }) .ToList() .OrderByDescending(s => s.MatchProb) .ToList(); return sortedAlbums.TakeWhile((x, i) => i == 0 || sortedAlbums[i - 1].MatchProb - x.MatchProb < fuzzGap) .TakeWhile((x, i) => x.MatchProb > fuzzThreshold || (i > 0 && sortedAlbums[i - 1].MatchProb > fuzzThreshold)) .Select(x => x.Album) .ToList(); } public List GetAllAlbums() { return _albumRepository.All().ToList(); } public Album GetAlbum(int albumId) { return _albumRepository.Get(albumId); } public List GetAlbums(IEnumerable albumIds) { return _albumRepository.Get(albumIds).ToList(); } public List GetAlbumsByArtist(int artistId) { return _albumRepository.GetAlbums(artistId).ToList(); } public List GetNextAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) { return _albumRepository.GetNextAlbums(artistMetadataIds).ToList(); } public List GetLastAlbumsByArtistMetadataId(IEnumerable artistMetadataIds) { return _albumRepository.GetLastAlbums(artistMetadataIds).ToList(); } public List GetAlbumsByArtistMetadataId(int artistMetadataId) { return _albumRepository.GetAlbumsByArtistMetadataId(artistMetadataId).ToList(); } public List GetAlbumsForRefresh(int artistMetadataId, List foreignIds) { return _albumRepository.GetAlbumsForRefresh(artistMetadataId, foreignIds); } public Album FindAlbumByRelease(string albumReleaseId) { return _albumRepository.FindAlbumByRelease(albumReleaseId); } public Album FindAlbumByTrackId(int trackId) { return _albumRepository.FindAlbumByTrack(trackId); } public void SetAddOptions(IEnumerable albums) { _albumRepository.SetFields(albums.ToList(), s => s.AddOptions); } public PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec) { var albumResult = _albumRepository.AlbumsWithoutFiles(pagingSpec); return albumResult; } public List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) { var albums = _albumRepository.AlbumsBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); return albums; } public List ArtistAlbumsBetweenDates(Artist artist, DateTime start, DateTime end, bool includeUnmonitored) { var albums = _albumRepository.ArtistAlbumsBetweenDates(artist, start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); return albums; } public List GetArtistAlbumsWithFiles(Artist artist) { return _albumRepository.GetArtistAlbumsWithFiles(artist); } public void InsertMany(List albums) { _albumRepository.InsertMany(albums); } public void UpdateMany(List albums) { _albumRepository.UpdateMany(albums); } public void DeleteMany(List albums) { _albumRepository.DeleteMany(albums); foreach (var album in albums) { _eventAggregator.PublishEvent(new AlbumDeletedEvent(album, false, false)); } } public Album UpdateAlbum(Album album) { var storedAlbum = GetAlbum(album.Id); var updatedAlbum = _albumRepository.Update(album); // If updatedAlbum has populated the Releases, populate in the storedAlbum too if (updatedAlbum.AlbumReleases.IsLoaded) { storedAlbum.AlbumReleases.LazyLoad(); } _eventAggregator.PublishEvent(new AlbumEditedEvent(updatedAlbum, storedAlbum)); return updatedAlbum; } public void SetAlbumMonitored(int albumId, bool monitored) { var album = _albumRepository.Get(albumId); _albumRepository.SetMonitoredFlat(album, monitored); // publish album edited event so artist stats update _eventAggregator.PublishEvent(new AlbumEditedEvent(album, album)); _logger.Debug("Monitored flag for Album:{0} was set to {1}", albumId, monitored); } public void SetMonitored(IEnumerable ids, bool monitored) { _albumRepository.SetMonitored(ids, monitored); // publish album edited event so artist stats update foreach (var album in _albumRepository.Get(ids)) { _eventAggregator.PublishEvent(new AlbumEditedEvent(album, album)); } } public void Handle(ArtistsDeletedEvent message) { // TODO Do this in one call instead of one for each artist? var albums = message.Artists.SelectMany(x => GetAlbumsByArtistMetadataId(x.ArtistMetadataId)).ToList(); DeleteMany(albums); } } }