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
+ };
+ }
+ }
+}