mirror of
https://github.com/Radarr/Radarr
synced 2025-12-06 16:32:36 +01:00
New: Keywords custom filter and autotagging for movies
This commit is contained in:
parent
4d3d46d796
commit
cb59ce891a
16 changed files with 115 additions and 6 deletions
|
|
@ -43,7 +43,8 @@
|
||||||
.physicalRelease,
|
.physicalRelease,
|
||||||
.digitalRelease,
|
.digitalRelease,
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.genres {
|
.genres,
|
||||||
|
.keywords {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface CssExports {
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'imdbRating': string;
|
'imdbRating': string;
|
||||||
'inCinemas': string;
|
'inCinemas': string;
|
||||||
|
'keywords': string;
|
||||||
'minimumAvailability': string;
|
'minimumAvailability': string;
|
||||||
'movieStatus': string;
|
'movieStatus': string;
|
||||||
'originalLanguage': string;
|
'originalLanguage': string;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
path,
|
path,
|
||||||
genres = [],
|
genres = [],
|
||||||
|
keywords = [],
|
||||||
ratings,
|
ratings,
|
||||||
popularity,
|
popularity,
|
||||||
certification,
|
certification,
|
||||||
|
|
@ -339,6 +340,20 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'keywords') {
|
||||||
|
const joinedKeywords = keywords.join(', ');
|
||||||
|
const truncatedKeywords =
|
||||||
|
keywords.length > 3
|
||||||
|
? `${keywords.slice(0, 3).join(', ')}...`
|
||||||
|
: joinedKeywords;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<span title={joinedKeywords}>{truncatedKeywords}</span>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'movieStatus') {
|
if (name === 'movieStatus') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@
|
||||||
.physicalRelease,
|
.physicalRelease,
|
||||||
.digitalRelease,
|
.digitalRelease,
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.genres {
|
.genres,
|
||||||
|
.keywords {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface CssExports {
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'imdbRating': string;
|
'imdbRating': string;
|
||||||
'inCinemas': string;
|
'inCinemas': string;
|
||||||
|
'keywords': string;
|
||||||
'minimumAvailability': string;
|
'minimumAvailability': string;
|
||||||
'movieStatus': string;
|
'movieStatus': string;
|
||||||
'originalLanguage': string;
|
'originalLanguage': string;
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ interface Movie extends ModelBase {
|
||||||
minimumAvailability: MovieAvailability;
|
minimumAvailability: MovieAvailability;
|
||||||
path: string;
|
path: string;
|
||||||
genres: string[];
|
genres: string[];
|
||||||
|
keywords: string[];
|
||||||
ratings: Ratings;
|
ratings: Ratings;
|
||||||
popularity: number;
|
popularity: number;
|
||||||
certification: string;
|
certification: string;
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,12 @@ export const defaultState = {
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'keywords',
|
||||||
|
label: () => translate('Keywords'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'movieStatus',
|
name: 'movieStatus',
|
||||||
label: () => translate('Status'),
|
label: () => translate('Status'),
|
||||||
|
|
@ -473,8 +479,8 @@ export const defaultState = {
|
||||||
label: () => translate('Genres'),
|
label: () => translate('Genres'),
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
optionsSelector: function(items) {
|
optionsSelector: function(items) {
|
||||||
const genreList = items.reduce((acc, movie) => {
|
const genreList = items.reduce((acc, { genres = [] }) => {
|
||||||
movie.genres.forEach((genre) => {
|
genres.forEach((genre) => {
|
||||||
acc.push({
|
acc.push({
|
||||||
id: genre,
|
id: genre,
|
||||||
name: genre
|
name: genre
|
||||||
|
|
@ -487,6 +493,27 @@ export const defaultState = {
|
||||||
return genreList.sort(sortByProp('name'));
|
return genreList.sort(sortByProp('name'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'keywords',
|
||||||
|
label: () => translate('Keywords'),
|
||||||
|
type: filterBuilderTypes.ARRAY,
|
||||||
|
optionsSelector: function(items) {
|
||||||
|
const keywordList = items.reduce((acc, { keywords = [] }) => {
|
||||||
|
keywords.forEach((keyword) => {
|
||||||
|
if (acc.findIndex((a) => a.id === keyword) === -1) {
|
||||||
|
acc.push({
|
||||||
|
id: keyword,
|
||||||
|
name: keyword
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return keywordList.sort(sortByProp('name'));
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'tmdbRating',
|
name: 'tmdbRating',
|
||||||
label: () => translate('TmdbRating'),
|
label: () => translate('TmdbRating'),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public class GenreSpecification : AutoTaggingSpecificationBase
|
||||||
{
|
{
|
||||||
private static readonly GenreSpecificationValidator Validator = new ();
|
private static readonly GenreSpecificationValidator Validator = new ();
|
||||||
|
|
||||||
public override int Order => 1;
|
public override int Order => 2;
|
||||||
public override string ImplementationName => "Genre";
|
public override string ImplementationName => "Genre";
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||||
|
{
|
||||||
|
public class KeywordSpecificationValidator : AbstractValidator<KeywordSpecification>
|
||||||
|
{
|
||||||
|
public KeywordSpecificationValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.Value).NotEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KeywordSpecification : AutoTaggingSpecificationBase
|
||||||
|
{
|
||||||
|
private static readonly KeywordSpecificationValidator Validator = new ();
|
||||||
|
|
||||||
|
public override int Order => 2;
|
||||||
|
public override string ImplementationName => "Keyword";
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "AutoTaggingSpecificationKeyword", Type = FieldType.Tag)]
|
||||||
|
public IEnumerable<string> Value { get; set; }
|
||||||
|
|
||||||
|
protected override bool IsSatisfiedByWithoutNegate(Movie movie)
|
||||||
|
{
|
||||||
|
return movie?.MovieMetadata?.Value?.Keywords.Any(keyword => Value.ContainsIgnoreCase(keyword)) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(242)]
|
||||||
|
public class add_movie_keywords : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("MovieMetadata").AddColumn("Keywords").AsString().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -119,6 +119,7 @@
|
||||||
"AutoTaggingNegateHelpText": "If checked, the auto tagging rule will not apply if this {implementationName} condition matches.",
|
"AutoTaggingNegateHelpText": "If checked, the auto tagging rule will not apply if this {implementationName} condition matches.",
|
||||||
"AutoTaggingRequiredHelpText": "This {implementationName} condition must match for the auto tagging rule to apply. Otherwise a single {implementationName} match is sufficient.",
|
"AutoTaggingRequiredHelpText": "This {implementationName} condition must match for the auto tagging rule to apply. Otherwise a single {implementationName} match is sufficient.",
|
||||||
"AutoTaggingSpecificationGenre": "Genre(s)",
|
"AutoTaggingSpecificationGenre": "Genre(s)",
|
||||||
|
"AutoTaggingSpecificationKeyword": "Keyword(s)",
|
||||||
"AutoTaggingSpecificationMaximumRuntime": "Maximum Runtime",
|
"AutoTaggingSpecificationMaximumRuntime": "Maximum Runtime",
|
||||||
"AutoTaggingSpecificationMaximumYear": "Maximum Year",
|
"AutoTaggingSpecificationMaximumYear": "Maximum Year",
|
||||||
"AutoTaggingSpecificationMinimumRuntime": "Minimum Runtime",
|
"AutoTaggingSpecificationMinimumRuntime": "Minimum Runtime",
|
||||||
|
|
@ -959,6 +960,7 @@
|
||||||
"KeyboardShortcutsMovieIndexScrollTop": "Movie Index: Scroll Top",
|
"KeyboardShortcutsMovieIndexScrollTop": "Movie Index: Scroll Top",
|
||||||
"KeyboardShortcutsOpenModal": "Open This Modal",
|
"KeyboardShortcutsOpenModal": "Open This Modal",
|
||||||
"KeyboardShortcutsSaveSettings": "Save Settings",
|
"KeyboardShortcutsSaveSettings": "Save Settings",
|
||||||
|
"Keywords": "Keywords",
|
||||||
"Label": "Label",
|
"Label": "Label",
|
||||||
"LabelIsRequired": "Label is required",
|
"LabelIsRequired": "Label is required",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public class MovieResource
|
||||||
public int? Runtime { get; set; }
|
public int? Runtime { get; set; }
|
||||||
public List<ImageResource> Images { get; set; }
|
public List<ImageResource> Images { get; set; }
|
||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
|
public List<string> Keywords { get; set; }
|
||||||
|
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
public DateTime? Premier { get; set; }
|
public DateTime? Premier { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,8 @@ public MovieMetadata MapMovie(MovieResource resource)
|
||||||
movie.Ratings = MapRatings(resource.MovieRatings) ?? new Ratings();
|
movie.Ratings = MapRatings(resource.MovieRatings) ?? new Ratings();
|
||||||
|
|
||||||
movie.TmdbId = resource.TmdbId;
|
movie.TmdbId = resource.TmdbId;
|
||||||
movie.Genres = resource.Genres;
|
movie.Genres = resource.Genres ?? new List<string>();
|
||||||
|
movie.Keywords = resource.Keywords ?? new List<string>();
|
||||||
movie.Images = resource.Images.Select(MapImage).ToList();
|
movie.Images = resource.Images.Select(MapImage).ToList();
|
||||||
|
|
||||||
movie.Recommendations = resource.Recommendations?.Select(r => r.TmdbId).ToList() ?? new List<int>();
|
movie.Recommendations = resource.Recommendations?.Select(r => r.TmdbId).ToList() ?? new List<int>();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public MovieMetadata()
|
||||||
Translations = new List<MovieTranslation>();
|
Translations = new List<MovieTranslation>();
|
||||||
Images = new List<MediaCover.MediaCover>();
|
Images = new List<MediaCover.MediaCover>();
|
||||||
Genres = new List<string>();
|
Genres = new List<string>();
|
||||||
|
Keywords = new List<string>();
|
||||||
OriginalLanguage = Language.English;
|
OriginalLanguage = Language.English;
|
||||||
Recommendations = new List<int>();
|
Recommendations = new List<int>();
|
||||||
Ratings = new Ratings();
|
Ratings = new Ratings();
|
||||||
|
|
@ -24,6 +25,7 @@ public MovieMetadata()
|
||||||
|
|
||||||
public List<MediaCover.MediaCover> Images { get; set; }
|
public List<MediaCover.MediaCover> Images { get; set; }
|
||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
|
public List<string> Keywords { get; set; }
|
||||||
public DateTime? InCinemas { get; set; }
|
public DateTime? InCinemas { get; set; }
|
||||||
public DateTime? PhysicalRelease { get; set; }
|
public DateTime? PhysicalRelease { get; set; }
|
||||||
public DateTime? DigitalRelease { get; set; }
|
public DateTime? DigitalRelease { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ private Movie RefreshMovieInfo(int movieId)
|
||||||
movieMetadata.Runtime = movieInfo.Runtime;
|
movieMetadata.Runtime = movieInfo.Runtime;
|
||||||
movieMetadata.Ratings = movieInfo.Ratings;
|
movieMetadata.Ratings = movieInfo.Ratings;
|
||||||
movieMetadata.Genres = movieInfo.Genres;
|
movieMetadata.Genres = movieInfo.Genres;
|
||||||
|
movieMetadata.Keywords = movieInfo.Keywords;
|
||||||
movieMetadata.Certification = movieInfo.Certification;
|
movieMetadata.Certification = movieInfo.Certification;
|
||||||
movieMetadata.InCinemas = movieInfo.InCinemas;
|
movieMetadata.InCinemas = movieInfo.InCinemas;
|
||||||
movieMetadata.Website = movieInfo.Website;
|
movieMetadata.Website = movieInfo.Website;
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ public MovieResource()
|
||||||
public string Folder { get; set; }
|
public string Folder { get; set; }
|
||||||
public string Certification { get; set; }
|
public string Certification { get; set; }
|
||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
|
public List<string> Keywords { get; set; }
|
||||||
public HashSet<int> Tags { get; set; }
|
public HashSet<int> Tags { get; set; }
|
||||||
public DateTime Added { get; set; }
|
public DateTime Added { get; set; }
|
||||||
public AddMovieOptions AddOptions { get; set; }
|
public AddMovieOptions AddOptions { get; set; }
|
||||||
|
|
@ -153,6 +154,7 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
|
||||||
Certification = model.MovieMetadata.Value.Certification,
|
Certification = model.MovieMetadata.Value.Certification,
|
||||||
Website = model.MovieMetadata.Value.Website,
|
Website = model.MovieMetadata.Value.Website,
|
||||||
Genres = model.MovieMetadata.Value.Genres,
|
Genres = model.MovieMetadata.Value.Genres,
|
||||||
|
Keywords = model.MovieMetadata.Value.Keywords,
|
||||||
Tags = model.Tags,
|
Tags = model.Tags,
|
||||||
Added = model.Added,
|
Added = model.Added,
|
||||||
AddOptions = model.AddOptions,
|
AddOptions = model.AddOptions,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue