From eaba78ad4a2f50cc60f1a05ac641ec9affe714a4 Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 17 Oct 2017 00:05:03 -0400 Subject: [PATCH] Add AlbumCutoffService and Refactor UI --- frontend/src/Store/Reducers/wantedReducers.js | 28 +++--- .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 46 ++------- src/Lidarr.Api.V3/Wanted/CutoffModule.cs | 14 ++- src/NzbDrone.Core/Music/AlbumCutoffService.cs | 65 ++++++++++++ src/NzbDrone.Core/Music/AlbumRepository.cs | 99 +++++++++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 6 files changed, 192 insertions(+), 61 deletions(-) create mode 100644 src/NzbDrone.Core/Music/AlbumCutoffService.cs diff --git a/frontend/src/Store/Reducers/wantedReducers.js b/frontend/src/Store/Reducers/wantedReducers.js index 1c00963fc..c126901c2 100644 --- a/frontend/src/Store/Reducers/wantedReducers.js +++ b/frontend/src/Store/Reducers/wantedReducers.js @@ -76,19 +76,19 @@ export const defaultState = { isSortable: true, isVisible: true }, + // { + // name: 'episode', + // label: 'Episode', + // isVisible: true + // }, { - name: 'episode', - label: 'Episode', - isVisible: true - }, - { - name: 'episodeTitle', - label: 'Episode Title', + name: 'albumTitle', + label: 'Album Title', isVisible: true }, { - name: 'airDateUtc', - label: 'Air Date', + name: 'releaseDate', + label: 'Release Date', isSortable: true, isVisible: true }, @@ -97,11 +97,11 @@ export const defaultState = { label: 'Language', isVisible: false }, - { - name: 'status', - label: 'Status', - isVisible: true - }, + // { + // name: 'status', + // label: 'Status', + // isVisible: true + // }, { name: 'actions', columnLabel: 'Actions', diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js index 89cfc5b49..e7f0f9172 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -3,7 +3,6 @@ import React from 'react'; import episodeEntities from 'Episode/episodeEntities'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; -import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; import TrackFileLanguageConnector from 'TrackFile/TrackFileLanguageConnector'; import ArtistNameLink from 'Artist/ArtistNameLink'; @@ -18,13 +17,7 @@ function CutoffUnmetRow(props) { id, trackFileId, artist, - seasonNumber, - episodeNumber, - absoluteEpisodeNumber, - sceneSeasonNumber, - sceneEpisodeNumber, - sceneAbsoluteEpisodeNumber, - airDateUtc, + releaseDate, title, isSelected, columns, @@ -54,33 +47,14 @@ function CutoffUnmetRow(props) { return ( ); } - if (name === 'episode') { - return ( - - - - ); - } - - if (name === 'episodeTitle') { + if (name === 'albumTitle') { return ( ); } @@ -155,13 +129,7 @@ CutoffUnmetRow.propTypes = { id: PropTypes.number.isRequired, trackFileId: PropTypes.number, artist: PropTypes.object.isRequired, - seasonNumber: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number, - sceneSeasonNumber: PropTypes.number, - sceneEpisodeNumber: PropTypes.number, - sceneAbsoluteEpisodeNumber: PropTypes.number, - airDateUtc: PropTypes.string.isRequired, + releaseDate: PropTypes.string.isRequired, title: PropTypes.string.isRequired, isSelected: PropTypes.bool, columns: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/src/Lidarr.Api.V3/Wanted/CutoffModule.cs b/src/Lidarr.Api.V3/Wanted/CutoffModule.cs index 70fc721ba..e104269fe 100644 --- a/src/Lidarr.Api.V3/Wanted/CutoffModule.cs +++ b/src/Lidarr.Api.V3/Wanted/CutoffModule.cs @@ -1,7 +1,6 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Music; -using NzbDrone.Core.Tv; //TODO Remove after EpisodeCutoffService is Refactored using NzbDrone.Core.ArtistStats; using NzbDrone.SignalR; using Lidarr.Api.V3.Albums; @@ -12,9 +11,9 @@ namespace Lidarr.Api.V3.Wanted { public class CutoffModule : AlbumModuleWithSignalR { - private readonly IEpisodeCutoffService _episodeCutoffService; + private readonly IAlbumCutoffService _albumCutoffService; - public CutoffModule(IEpisodeCutoffService episodeCutoffService, + public CutoffModule(IAlbumCutoffService albumCutoffService, IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, @@ -22,7 +21,7 @@ namespace Lidarr.Api.V3.Wanted IBroadcastSignalRMessage signalRBroadcaster) : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") { - _episodeCutoffService = episodeCutoffService; + _albumCutoffService = albumCutoffService; GetResourcePaged = GetCutoffUnmetAlbums; } @@ -37,7 +36,6 @@ namespace Lidarr.Api.V3.Wanted }; var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); - var includeTrackFile = Request.GetBooleanQueryParameter("includeTrackFile"); if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") { @@ -48,9 +46,9 @@ namespace Lidarr.Api.V3.Wanted pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; } - //var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeSeries, includeEpisodeFile)); - return null; - //return resource; + var resource = ApplyToPage(_albumCutoffService.AlbumsWhereCutoffUnmet, pagingSpec, v => MapToResource(v, includeArtist)); + + return resource; } } } diff --git a/src/NzbDrone.Core/Music/AlbumCutoffService.cs b/src/NzbDrone.Core/Music/AlbumCutoffService.cs new file mode 100644 index 000000000..58eb2cf05 --- /dev/null +++ b/src/NzbDrone.Core/Music/AlbumCutoffService.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Music +{ + public interface IAlbumCutoffService + { + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec); + } + + public class AlbumCutoffService : IAlbumCutoffService + { + private readonly IAlbumRepository _albumRepository; + private readonly IProfileService _profileService; + private readonly ILanguageProfileService _languageProfileService; + private readonly Logger _logger; + + public AlbumCutoffService(IAlbumRepository albumRepository, IProfileService profileService, ILanguageProfileService languageProfileService, Logger logger) + { + _albumRepository = albumRepository; + _profileService = profileService; + _languageProfileService = languageProfileService; + _logger = logger; + } + + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec) + { + var qualitiesBelowCutoff = new List(); + var languagesBelowCutoff = new List(); + var profiles = _profileService.All(); + var languageProfiles = _languageProfileService.All(); + + //Get all items less than the cutoff + foreach (var profile in profiles) + { + var cutoffIndex = profile.Items.FindIndex(v => v.Quality == profile.Cutoff); + var belowCutoff = profile.Items.Take(cutoffIndex).ToList(); + + if (belowCutoff.Any()) + { + qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id))); + } + } + + foreach (var profile in languageProfiles) + { + var languageCutoffIndex = profile.Languages.FindIndex(v => v.Language == profile.Cutoff); + var belowLanguageCutoff = profile.Languages.Take(languageCutoffIndex).ToList(); + + if (belowLanguageCutoff.Any()) + { + languagesBelowCutoff.Add(new LanguagesBelowCutoff(profile.Id, belowLanguageCutoff.Select(l => l.Language.Id))); + } + } + + return _albumRepository.AlbumsWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff); + } + } +} diff --git a/src/NzbDrone.Core/Music/AlbumRepository.cs b/src/NzbDrone.Core/Music/AlbumRepository.cs index 83e6d9296..439e9c698 100644 --- a/src/NzbDrone.Core/Music/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/AlbumRepository.cs @@ -5,6 +5,8 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using System.Collections.Generic; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; using Marr.Data.QGen; namespace NzbDrone.Core.Music @@ -18,6 +20,7 @@ namespace NzbDrone.Core.Music Album FindByArtistAndName(string artistName, string cleanTitle); Album FindById(string spotifyId); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); + PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff); List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Album album, bool monitored); void SetMonitored(IEnumerable ids, bool monitored); @@ -61,6 +64,15 @@ namespace NzbDrone.Core.Music return pagingSpec; } + public PagingSpec AlbumsWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + + pagingSpec.TotalRecords = GetCutOffAlbumsQueryCount(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff); + pagingSpec.Records = GetCutOffAlbumsQuery(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff).ToList(); + + return pagingSpec; + } + public List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) { var query = Query.Join(JoinType.Inner, e => e.Artist, (e, s) => e.ArtistId == s.Id) @@ -148,6 +160,93 @@ namespace NzbDrone.Core.Music currentTime.ToString("yyyy-MM-dd HH:mm:ss")); } + private QueryBuilder GetCutOffAlbumsQuery(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + string sortKey; + string monitored = "(Albums.[Monitored] = 0) OR (Artists.[Monitored] = 0)"; + + if (pagingSpec.FilterExpression.ToString().Contains("True")) + { + monitored = "(Albums.[Monitored] = 1) AND (Artists.[Monitored] = 1)"; + } + + if (pagingSpec.SortKey == "releaseDate") + { + sortKey = "Albums." + pagingSpec.SortKey; + } + else if (pagingSpec.SortKey == "artist.sortName") + { + sortKey = "Artists." + pagingSpec.SortKey.Split('.').Last(); + } + else + { + sortKey = "Albums.releaseDate"; + } + + string query = string.Format("SELECT Albums.* FROM(SELECT TrackFiles.AlbumId, TrackFiles.Language, COUNT(*) AS FileCount, " + + " MIN(Quality) AS MinQuality FROM TrackFiles GROUP BY TrackFiles.ArtistId, TrackFiles.AlbumId) as TrackFiles" + + " LEFT OUTER JOIN Albums ON TrackFiles.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE ({0}) AND ({1} OR {2})" + + " GROUP BY TrackFiles.AlbumId" + + " ORDER BY {3} {4} LIMIT {5} OFFSET {6}", + monitored, BuildQualityCutoffWhereClause(qualitiesBelowCutoff), BuildLanguageCutoffWhereClause(languagesBelowCutoff), sortKey, pagingSpec.ToSortDirection(), pagingSpec.PageSize, pagingSpec.PagingOffset()); + + return Query.QueryText(query); + + } + + private int GetCutOffAlbumsQueryCount(PagingSpec pagingSpec, List qualitiesBelowCutoff, List languagesBelowCutoff) + { + var monitored = 0; + + if (pagingSpec.FilterExpression.ToString().Contains("True")) + { + monitored = 1; + } + + string query = string.Format("SELECT Albums.* FROM (SELECT TrackFiles.AlbumId, TrackFiles.Language, COUNT(*) AS FileCount," + + " MIN(Quality) AS MinQuality FROM TrackFiles GROUP BY TrackFiles.ArtistId, TrackFiles.AlbumId) as TrackFiles" + + " LEFT OUTER JOIN Albums ON TrackFiles.AlbumId == Albums.Id" + + " LEFT OUTER JOIN Artists ON Albums.ArtistId == Artists.Id" + + " WHERE ({0}) AND ({1} OR {2})" + + " GROUP BY TrackFiles.AlbumId", + monitored, BuildQualityCutoffWhereClause(qualitiesBelowCutoff), BuildLanguageCutoffWhereClause(languagesBelowCutoff)); + + return Query.QueryText(query).Count(); + } + + + private string BuildLanguageCutoffWhereClause(List languagesBelowCutoff) + { + var clauses = new List(); + + foreach (var language in languagesBelowCutoff) + { + foreach (var belowCutoff in language.LanguageIds) + { + clauses.Add(String.Format("(Artists.[LanguageProfileId] = {0} AND TrackFiles.[Language] = {1})", language.ProfileId, belowCutoff)); + } + } + + return String.Format("({0})", String.Join(" OR ", clauses)); + } + + private string BuildQualityCutoffWhereClause(List qualitiesBelowCutoff) + { + var clauses = new List(); + + foreach (var profile in qualitiesBelowCutoff) + { + foreach (var belowCutoff in profile.QualityIds) + { + clauses.Add(string.Format("(Artists.[ProfileId] = {0} AND TrackFiles.MinQuality LIKE '%_quality_: {1},%')", profile.ProfileId, belowCutoff)); + } + } + + return string.Format("({0})", string.Join(" OR ", clauses)); + } + public void SetMonitoredFlat(Album album, bool monitored) { album.Monitored = monitored; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index dbe1dfa8d..a996e2f07 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -866,6 +866,7 @@ +