From 35651ac59b15d0558ef1734ed180753d1182f00b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 16 Jan 2024 20:52:07 +0200 Subject: [PATCH] New: Release Groups for movie table index * New: Release Group for movie table index Co-authored-by: Qstick * fixup! New: Release Group for movie table index --------- Co-authored-by: Qstick --- .../Overview/CollectionMovieLabel.js | 6 +- .../Movie/Delete/DeleteMovieModalContent.js | 12 ++- frontend/src/Movie/Details/MovieDetails.js | 12 ++- frontend/src/Movie/Index/MovieIndexFooter.tsx | 16 ++-- .../Index/Overview/MovieIndexOverview.tsx | 5 +- .../Movie/Index/Posters/MovieIndexPoster.tsx | 5 +- .../src/Movie/Index/Table/MovieIndexRow.css | 1 + .../Movie/Index/Table/MovieIndexRow.css.d.ts | 1 + .../src/Movie/Index/Table/MovieIndexRow.tsx | 19 +++- .../Index/Table/MovieIndexTableHeader.css | 1 + .../Table/MovieIndexTableHeader.css.d.ts | 1 + frontend/src/Movie/Movie.ts | 8 +- frontend/src/Store/Actions/movieActions.js | 22 +++++ .../src/Store/Actions/movieIndexActions.js | 39 ++++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + .../MovieStats/MovieStatistics.cs | 35 ++++++++ .../MovieStats/MovieStatisticsRepository.cs | 88 +++++++++++++++++++ .../MovieStats/MovieStatisticsService.cs | 40 +++++++++ src/Radarr.Api.V3/Movies/MovieController.cs | 33 +++++++ src/Radarr.Api.V3/Movies/MovieResource.cs | 9 +- .../Movies/MovieStatisticsResource.cs | 30 +++++++ 21 files changed, 358 insertions(+), 26 deletions(-) create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatistics.cs create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs create mode 100644 src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs create mode 100644 src/Radarr.Api.V3/Movies/MovieStatisticsResource.cs diff --git a/frontend/src/Collection/Overview/CollectionMovieLabel.js b/frontend/src/Collection/Overview/CollectionMovieLabel.js index 14f698512..9e827999e 100644 --- a/frontend/src/Collection/Overview/CollectionMovieLabel.js +++ b/frontend/src/Collection/Overview/CollectionMovieLabel.js @@ -74,11 +74,7 @@ CollectionMovieLabel.propTypes = { CollectionMovieLabel.defaultProps = { isSaving: false, - statistics: { - episodeFileCount: 0, - totalEpisodeCount: 0, - percentOfEpisodes: 0 - } + statistics: {} }; export default CollectionMovieLabel; diff --git a/frontend/src/Movie/Delete/DeleteMovieModalContent.js b/frontend/src/Movie/Delete/DeleteMovieModalContent.js index 577ad443b..fea42f444 100644 --- a/frontend/src/Movie/Delete/DeleteMovieModalContent.js +++ b/frontend/src/Movie/Delete/DeleteMovieModalContent.js @@ -50,12 +50,16 @@ class DeleteMovieModalContent extends Component { title, path, hasFile, + statistics, deleteOptions, - sizeOnDisk, onModalClose, onDeleteOptionChange } = this.props; + const { + sizeOnDisk = 0 + } = statistics; + const deleteFiles = this.state.deleteFiles; const addImportExclusion = deleteOptions.addImportExclusion; @@ -151,12 +155,16 @@ class DeleteMovieModalContent extends Component { DeleteMovieModalContent.propTypes = { title: PropTypes.string.isRequired, path: PropTypes.string.isRequired, + statistics: PropTypes.object.isRequired, hasFile: PropTypes.bool.isRequired, - sizeOnDisk: PropTypes.number.isRequired, deleteOptions: PropTypes.object.isRequired, onDeleteOptionChange: PropTypes.func.isRequired, onDeletePress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; +DeleteMovieModalContent.defaultProps = { + statistics: {} +}; + export default DeleteMovieModalContent; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 95fc8bebf..4f8d1f895 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -238,7 +238,7 @@ class MovieDetails extends Component { certification, ratings, path, - sizeOnDisk, + statistics, qualityProfileId, monitored, studio, @@ -267,6 +267,10 @@ class MovieDetails extends Component { movieRuntimeFormat } = this.props; + const { + sizeOnDisk = 0 + } = statistics; + const { isOrganizeModalOpen, isEditMovieModalOpen, @@ -734,7 +738,7 @@ MovieDetails.propTypes = { certification: PropTypes.string, ratings: PropTypes.object.isRequired, path: PropTypes.string.isRequired, - sizeOnDisk: PropTypes.number.isRequired, + statistics: PropTypes.object.isRequired, qualityProfileId: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, @@ -773,9 +777,9 @@ MovieDetails.propTypes = { MovieDetails.defaultProps = { genres: [], + statistics: {}, tags: [], - isSaving: false, - sizeOnDisk: 0 + isSaving: false }; export default MovieDetails; diff --git a/frontend/src/Movie/Index/MovieIndexFooter.tsx b/frontend/src/Movie/Index/MovieIndexFooter.tsx index b0de466e7..c7f56abeb 100644 --- a/frontend/src/Movie/Index/MovieIndexFooter.tsx +++ b/frontend/src/Movie/Index/MovieIndexFooter.tsx @@ -17,13 +17,13 @@ function createUnoptimizedSelector() { createClientSideCollectionSelector('movies', 'movieIndex'), (movies: MoviesAppState) => { return movies.items.map((m) => { - const { monitored, status, hasFile, sizeOnDisk } = m; + const { monitored, status, hasFile, statistics } = m; return { monitored, status, hasFile, - sizeOnDisk, + statistics, }; }); } @@ -44,16 +44,20 @@ export default function MovieIndexFooter() { let monitored = 0; let totalFileSize = 0; - movies.forEach((s) => { - if (s.hasFile) { + movies.forEach((m) => { + const { statistics = { sizeOnDisk: 0 } } = m; + + const { sizeOnDisk = 0 } = statistics; + + if (m.hasFile) { movieFiles += 1; } - if (s.monitored) { + if (m.monitored) { monitored++; } - totalFileSize += s.sizeOnDisk; + totalFileSize += sizeOnDisk; }); return ( diff --git a/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx b/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx index 995034a15..29d4ace25 100644 --- a/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx +++ b/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx @@ -13,6 +13,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; +import { Statistics } from 'Movie/Movie'; import MoviePoster from 'Movie/MoviePoster'; import { executeCommand } from 'Store/Actions/commandActions'; import dimensions from 'Styles/Variables/dimensions'; @@ -66,17 +67,19 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) { status, path, overview, + statistics = {} as Statistics, images, hasFile, isAvailable, tmdbId, imdbId, studio, - sizeOnDisk, added, youTubeTrailerId, } = movie; + const { sizeOnDisk = 0 } = statistics; + const dispatch = useDispatch(); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx index f6078f123..1b2c3569d 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx @@ -16,6 +16,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; +import { Statistics } from 'Movie/Movie'; import MoviePoster from 'Movie/MoviePoster'; import { executeCommand } from 'Store/Actions/commandActions'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; @@ -75,12 +76,14 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { path, movieFile, ratings, - sizeOnDisk, + statistics = {} as Statistics, certification, originalTitle, originalLanguage, } = movie; + const { sizeOnDisk = 0 } = statistics; + const dispatch = useDispatch(); const [hasPosterError, setHasPosterError] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css b/frontend/src/Movie/Index/Table/MovieIndexRow.css index 67719a637..9a7f28b72 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css @@ -38,6 +38,7 @@ flex: 1 0 125px; } +.releaseGroups, .inCinemas, .physicalRelease, .digitalRelease, diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts b/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts index 407e1d144..1ab4f519c 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css.d.ts @@ -20,6 +20,7 @@ interface CssExports { 'physicalRelease': string; 'popularity': string; 'qualityProfileId': string; + 'releaseGroups': string; 'rottenTomatoesRating': string; 'runtime': string; 'sizeOnDisk': string; diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx index 78806c08a..22bf5f4cb 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx @@ -19,6 +19,7 @@ import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector'; +import { Statistics } from 'Movie/Movie'; import MoviePopularityIndex from 'Movie/MoviePopularityIndex'; import MovieTitleLink from 'Movie/MovieTitleLink'; import { executeCommand } from 'Store/Actions/commandActions'; @@ -60,6 +61,7 @@ function MovieIndexRow(props: MovieIndexRowProps) { originalLanguage, originalTitle, added, + statistics = {} as Statistics, year, inCinemas, digitalRelease, @@ -67,7 +69,6 @@ function MovieIndexRow(props: MovieIndexRowProps) { runtime, minimumAvailability, path, - sizeOnDisk, genres = [], ratings, popularity, @@ -82,6 +83,8 @@ function MovieIndexRow(props: MovieIndexRowProps) { isSaving = false, } = movie; + const { sizeOnDisk = 0, releaseGroups = [] } = statistics; + const dispatch = useDispatch(); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); @@ -380,6 +383,20 @@ function MovieIndexRow(props: MovieIndexRowProps) { ); } + if (name === 'releaseGroups') { + const joinedReleaseGroups = releaseGroups.join(', '); + const truncatedReleaseGroups = + releaseGroups.length > 3 + ? `${releaseGroups.slice(0, 3).join(', ')}...` + : joinedReleaseGroups; + + return ( + + {truncatedReleaseGroups} + + ); + } + if (name === 'tags') { return ( diff --git a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css index f6d937857..4ad0acb1c 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css +++ b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css @@ -31,6 +31,7 @@ flex: 1 0 125px; } +.releaseGroups, .inCinemas, .physicalRelease, .digitalRelease, diff --git a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts index 8a8be09d4..29c46c5c3 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts +++ b/frontend/src/Movie/Index/Table/MovieIndexTableHeader.css.d.ts @@ -17,6 +17,7 @@ interface CssExports { 'physicalRelease': string; 'popularity': string; 'qualityProfileId': string; + 'releaseGroups': string; 'rottenTomatoesRating': string; 'runtime': string; 'sizeOnDisk': string; diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index 6301734ad..59e337515 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -12,6 +12,12 @@ export interface Collection { title: string; } +export interface Statistics { + movieFileCount: number; + releaseGroups: string[]; + sizeOnDisk: number; +} + export interface Ratings { imdb: object; tmdb: object; @@ -42,11 +48,11 @@ interface Movie extends ModelBase { runtime: number; minimumAvailability: string; path: string; - sizeOnDisk: number; genres: string[]; ratings: Ratings; popularity: number; certification: string; + statistics: Statistics; tags: number[]; images: Image[]; movieFile: MovieFile; diff --git a/frontend/src/Store/Actions/movieActions.js b/frontend/src/Store/Actions/movieActions.js index 9f2526492..e9bf36a4f 100644 --- a/frontend/src/Store/Actions/movieActions.js +++ b/frontend/src/Store/Actions/movieActions.js @@ -128,6 +128,22 @@ export const filterPredicates = { return predicate(originalLanguage ? originalLanguage.name : '', filterValue); }, + releaseGroups: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + const { statistics = {} } = item; + const { releaseGroups = [] } = statistics; + + return predicate(releaseGroups, filterValue); + }, + + sizeOnDisk: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + const { statistics = {} } = item; + const sizeOnDisk = statistics && statistics.sizeOnDisk ? statistics.sizeOnDisk : 0; + + return predicate(sizeOnDisk, filterValue); + }, + inCinemas: function(item, filterValue, type) { return dateFilterPredicate(item.inCinemas, filterValue, type); }, @@ -290,6 +306,12 @@ export const sortPredicates = { } return Number.MAX_VALUE; + }, + + sizeOnDisk: function(item) { + const { statistics = {} } = item; + + return statistics.sizeOnDisk || 0; } }; diff --git a/frontend/src/Store/Actions/movieIndexActions.js b/frontend/src/Store/Actions/movieIndexActions.js index 88359be69..7fc5e8c52 100644 --- a/frontend/src/Store/Actions/movieIndexActions.js +++ b/frontend/src/Store/Actions/movieIndexActions.js @@ -206,6 +206,12 @@ export const defaultState = { isSortable: true, isVisible: false }, + { + name: 'releaseGroups', + label: () => translate('ReleaseGroup'), + isSortable: true, + isVisible: false + }, { name: 'tags', label: () => translate('Tags'), @@ -241,6 +247,17 @@ export const defaultState = { return originalLanguage.name; }, + releaseGroups: function(item) { + const { statistics = {} } = item; + const { releaseGroups = [] } = statistics; + + return releaseGroups.length ? + releaseGroups + .map((group) => group.toLowerCase()) + .sort((a, b) => a.localeCompare(b)) : + undefined; + }, + imdbRating: function(item) { const { ratings = {} } = item; @@ -313,6 +330,28 @@ export const defaultState = { return collectionList.sort(sortByName); } }, + { + name: 'releaseGroups', + label: () => translate('ReleaseGroups'), + type: filterBuilderTypes.ARRAY, + optionsSelector: function(items) { + const groupList = items.reduce((acc, movie) => { + const { statistics = {} } = movie; + const { releaseGroups = [] } = statistics; + + releaseGroups.forEach((releaseGroup) => { + acc.push({ + id: releaseGroup, + name: releaseGroup + }); + }); + + return acc; + }, []); + + return groupList.sort(sortByName); + } + }, { name: 'status', label: () => translate('ReleaseStatus'), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 33ef89571..8a78c4f52 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1114,6 +1114,7 @@ "ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid {appName} release branch, you will not receive updates", "ReleaseDates": "Release Dates", "ReleaseGroup": "Release Group", + "ReleaseGroups": "Release Groups", "ReleaseHash": "Release Hash", "ReleaseProfiles": "Release Profiles", "ReleaseProfilesLoadError": "Unable to load Release Profiles", diff --git a/src/NzbDrone.Core/MovieStats/MovieStatistics.cs b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs new file mode 100644 index 000000000..ba8cc7964 --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatistics.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.MovieStats +{ + public class MovieStatistics : ResultSet + { + public int MovieId { get; set; } + public int MovieFileCount { get; set; } + public long SizeOnDisk { get; set; } + public string ReleaseGroupsString { get; set; } + + public List ReleaseGroups + { + get + { + var releaseGroups = new List(); + + if (ReleaseGroupsString.IsNotNullOrWhiteSpace()) + { + releaseGroups = ReleaseGroupsString + .Split('|') + .Distinct() + .Where(rg => rg.IsNotNullOrWhiteSpace()) + .OrderBy(rg => rg) + .ToList(); + } + + return releaseGroups; + } + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs new file mode 100644 index 000000000..69505e159 --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsRepository.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using Dapper; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsRepository + { + List MovieStatistics(); + List MovieStatistics(int movieId); + } + + public class MovieStatisticsRepository : IMovieStatisticsRepository + { + private const string _selectMoviesTemplate = "SELECT /**select**/ FROM \"Movies\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + private const string _selectMovieFilesTemplate = "SELECT /**select**/ FROM \"MovieFiles\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/ /**orderby**/"; + + private readonly IMainDatabase _database; + + public MovieStatisticsRepository(IMainDatabase database) + { + _database = database; + } + + public List MovieStatistics() + { + return MapResults(Query(MoviesBuilder(), _selectMoviesTemplate), + Query(MovieFilesBuilder(), _selectMovieFilesTemplate)); + } + + public List MovieStatistics(int movieId) + { + return MapResults(Query(MoviesBuilder().Where(x => x.Id == movieId), _selectMoviesTemplate), + Query(MovieFilesBuilder().Where(x => x.MovieId == movieId), _selectMovieFilesTemplate)); + } + + private List MapResults(List moviesResult, List filesResult) + { + moviesResult.ForEach(e => + { + var file = filesResult.SingleOrDefault(f => f.MovieId == e.MovieId); + + e.SizeOnDisk = file?.SizeOnDisk ?? 0; + e.ReleaseGroupsString = file?.ReleaseGroupsString; + }); + + return moviesResult; + } + + private List Query(SqlBuilder builder, string template) + { + var sql = builder.AddTemplate(template).LogQuery(); + + using var conn = _database.OpenConnection(); + + return conn.Query(sql.RawSql, sql.Parameters).ToList(); + } + + private SqlBuilder MoviesBuilder() + { + return new SqlBuilder(_database.DatabaseType) + .Select(@"""Movies"".""Id"" AS MovieId, + SUM(CASE WHEN ""MovieFileId"" > 0 THEN 1 ELSE 0 END) AS MovieFileCount") + .GroupBy(x => x.Id); + } + + private SqlBuilder MovieFilesBuilder() + { + if (_database.DatabaseType == DatabaseType.SQLite) + { + return new SqlBuilder(_database.DatabaseType) + .Select(@"""MovieId"", + SUM(COALESCE(""Size"", 0)) AS SizeOnDisk, + GROUP_CONCAT(""ReleaseGroup"", '|') AS ReleaseGroupsString") + .GroupBy(x => x.MovieId); + } + + return new SqlBuilder(_database.DatabaseType) + .Select(@"""MovieId"", + SUM(COALESCE(""Size"", 0)) AS SizeOnDisk, + string_agg(""ReleaseGroup"", '|') AS ReleaseGroupsString") + .GroupBy(x => x.MovieId); + } + } +} diff --git a/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs new file mode 100644 index 000000000..1165a454c --- /dev/null +++ b/src/NzbDrone.Core/MovieStats/MovieStatisticsService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.MovieStats +{ + public interface IMovieStatisticsService + { + List MovieStatistics(); + MovieStatistics MovieStatistics(int movieId); + } + + public class MovieStatisticsService : IMovieStatisticsService + { + private readonly IMovieStatisticsRepository _movieStatisticsRepository; + + public MovieStatisticsService(IMovieStatisticsRepository movieStatisticsRepository) + { + _movieStatisticsRepository = movieStatisticsRepository; + } + + public List MovieStatistics() + { + var movieStatistics = _movieStatisticsRepository.MovieStatistics(); + + return movieStatistics.GroupBy(m => m.MovieId).Select(m => m.First()).ToList(); + } + + public MovieStatistics MovieStatistics(int movieId) + { + var stats = _movieStatisticsRepository.MovieStatistics(movieId); + + if (stats == null || stats.Count == 0) + { + return new MovieStatistics(); + } + + return stats.First(); + } + } +} diff --git a/src/Radarr.Api.V3/Movies/MovieController.cs b/src/Radarr.Api.V3/Movies/MovieController.cs index e9683b2ea..165182794 100644 --- a/src/Radarr.Api.V3/Movies/MovieController.cs +++ b/src/Radarr.Api.V3/Movies/MovieController.cs @@ -20,6 +20,7 @@ using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Events; using NzbDrone.Core.Movies.Translations; +using NzbDrone.Core.MovieStats; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; @@ -43,6 +44,7 @@ namespace Radarr.Api.V3.Movies private readonly IMovieService _moviesService; private readonly IMovieTranslationService _movieTranslationService; private readonly IAddMovieService _addMovieService; + private readonly IMovieStatisticsService _movieStatisticsService; private readonly IMapCoversToLocal _coverMapper; private readonly IManageCommandQueue _commandQueueManager; private readonly IRootFolderService _rootFolderService; @@ -54,6 +56,7 @@ namespace Radarr.Api.V3.Movies IMovieService moviesService, IMovieTranslationService movieTranslationService, IAddMovieService addMovieService, + IMovieStatisticsService movieStatisticsService, IMapCoversToLocal coverMapper, IManageCommandQueue commandQueueManager, IRootFolderService rootFolderService, @@ -74,6 +77,7 @@ namespace Radarr.Api.V3.Movies _moviesService = moviesService; _movieTranslationService = movieTranslationService; _addMovieService = addMovieService; + _movieStatisticsService = movieStatisticsService; _qualityUpgradableSpecification = qualityUpgradableSpecification; _configService = configService; _coverMapper = coverMapper; @@ -125,6 +129,7 @@ namespace Radarr.Api.V3.Movies } else { + var movieStats = _movieStatisticsService.MovieStatistics(); var configLanguage = (Language)_configService.MovieInfoLanguage; var availDelay = _configService.AvailabilityDelay; @@ -134,6 +139,7 @@ namespace Radarr.Api.V3.Movies .GetAllTranslationsForLanguage(configLanguage); var tdict = translations.ToDictionary(x => x.MovieMetadataId); + var sdict = movieStats.ToDictionary(x => x.MovieId); if (!excludeLocalCovers) { @@ -155,6 +161,8 @@ namespace Radarr.Api.V3.Movies MapCoversToLocal(moviesResources, coverFileInfos); } + LinkMovieStatistics(moviesResources, sdict); + var rootFolders = _rootFolderService.All(); moviesResources.ForEach(m => m.RootFolderPath = _rootFolderService.GetBestRootFolderPath(m.Path, rootFolders)); @@ -166,6 +174,7 @@ namespace Radarr.Api.V3.Movies protected override MovieResource GetResourceById(int id) { var movie = _moviesService.GetMovie(id); + return MapToResource(movie); } @@ -183,6 +192,7 @@ namespace Radarr.Api.V3.Movies var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification); MapCoversToLocal(resource); + FetchAndLinkMovieStatistics(resource); resource.RootFolderPath = _rootFolderService.GetBestRootFolderPath(resource.Path); @@ -278,6 +288,29 @@ namespace Radarr.Api.V3.Movies _coverMapper.ConvertToLocalUrls(movies.Select(x => Tuple.Create(x.Id, x.Images.AsEnumerable())), coverFileInfos); } + private void FetchAndLinkMovieStatistics(MovieResource resource) + { + LinkMovieStatistics(resource, _movieStatisticsService.MovieStatistics(resource.Id)); + } + + private void LinkMovieStatistics(List resources, Dictionary sDict) + { + foreach (var movie in resources) + { + if (sDict.TryGetValue(movie.Id, out var stats)) + { + LinkMovieStatistics(movie, stats); + } + } + } + + private void LinkMovieStatistics(MovieResource resource, MovieStatistics movieStatistics) + { + resource.Statistics = movieStatistics.ToResource(); + resource.HasFile = movieStatistics.MovieFileCount > 0; + resource.SizeOnDisk = movieStatistics.SizeOnDisk; + } + [NonAction] public void Handle(MovieFileImportedEvent message) { diff --git a/src/Radarr.Api.V3/Movies/MovieResource.cs b/src/Radarr.Api.V3/Movies/MovieResource.cs index 344f5a772..f0b76a597 100644 --- a/src/Radarr.Api.V3/Movies/MovieResource.cs +++ b/src/Radarr.Api.V3/Movies/MovieResource.cs @@ -47,7 +47,6 @@ namespace Radarr.Api.V3.Movies // public bool Downloaded { get; set; } public string RemotePoster { get; set; } public int Year { get; set; } - public bool HasFile { get; set; } public string YouTubeTrailerId { get; set; } public string Studio { get; set; } @@ -55,6 +54,9 @@ namespace Radarr.Api.V3.Movies public string Path { get; set; } public int QualityProfileId { get; set; } + // Compatibility + public bool HasFile { get; set; } + // Editing Only public bool Monitored { get; set; } public MovieStatusType MinimumAvailability { get; set; } @@ -77,6 +79,7 @@ namespace Radarr.Api.V3.Movies public MovieFileResource MovieFile { get; set; } public MovieCollectionResource Collection { get; set; } public float Popularity { get; set; } + public MovieStatisticsResource Statistics { get; set; } } public static class MovieResourceMapper @@ -88,8 +91,6 @@ namespace Radarr.Api.V3.Movies return null; } - var size = model.MovieFile?.Size ?? 0; - var movieFile = model.MovieFile?.ToResource(model, upgradableSpecification, formatCalculationService); var translatedTitle = movieTranslation?.Title ?? model.Title; @@ -108,9 +109,7 @@ namespace Radarr.Api.V3.Movies InCinemas = model.MovieMetadata.Value.InCinemas, PhysicalRelease = model.MovieMetadata.Value.PhysicalRelease, DigitalRelease = model.MovieMetadata.Value.DigitalRelease, - HasFile = model.HasFile, - SizeOnDisk = size, Status = model.MovieMetadata.Value.Status, Overview = translatedOverview, diff --git a/src/Radarr.Api.V3/Movies/MovieStatisticsResource.cs b/src/Radarr.Api.V3/Movies/MovieStatisticsResource.cs new file mode 100644 index 000000000..e99220e8d --- /dev/null +++ b/src/Radarr.Api.V3/Movies/MovieStatisticsResource.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NzbDrone.Core.MovieStats; + +namespace Radarr.Api.V3.Movies +{ + public class MovieStatisticsResource + { + public int MovieFileCount { get; set; } + public long SizeOnDisk { get; set; } + public List ReleaseGroups { get; set; } + } + + public static class MovieStatisticsResourceMapper + { + public static MovieStatisticsResource ToResource(this MovieStatistics model) + { + if (model == null) + { + return null; + } + + return new MovieStatisticsResource + { + MovieFileCount = model.MovieFileCount, + SizeOnDisk = model.SizeOnDisk, + ReleaseGroups = model.ReleaseGroups + }; + } + } +}