New: Keywords custom filter and autotagging for movies

This commit is contained in:
Bogdan 2025-05-23 21:29:31 +03:00
parent 4d3d46d796
commit cb59ce891a
16 changed files with 115 additions and 6 deletions

View file

@ -43,7 +43,8 @@
.physicalRelease,
.digitalRelease,
.releaseDate,
.genres {
.genres,
.keywords {
composes: cell;
flex: 0 0 180px;

View file

@ -12,6 +12,7 @@ interface CssExports {
'genres': string;
'imdbRating': string;
'inCinemas': string;
'keywords': string;
'minimumAvailability': string;
'movieStatus': string;
'originalLanguage': string;

View file

@ -72,6 +72,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
minimumAvailability,
path,
genres = [],
keywords = [],
ratings,
popularity,
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') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>

View file

@ -36,7 +36,8 @@
.physicalRelease,
.digitalRelease,
.releaseDate,
.genres {
.genres,
.keywords {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 180px;

View file

@ -9,6 +9,7 @@ interface CssExports {
'genres': string;
'imdbRating': string;
'inCinemas': string;
'keywords': string;
'minimumAvailability': string;
'movieStatus': string;
'originalLanguage': string;

View file

@ -82,6 +82,7 @@ interface Movie extends ModelBase {
minimumAvailability: MovieAvailability;
path: string;
genres: string[];
keywords: string[];
ratings: Ratings;
popularity: number;
certification: string;

View file

@ -181,6 +181,12 @@ export const defaultState = {
isSortable: false,
isVisible: false
},
{
name: 'keywords',
label: () => translate('Keywords'),
isSortable: false,
isVisible: false
},
{
name: 'movieStatus',
label: () => translate('Status'),
@ -473,8 +479,8 @@ export const defaultState = {
label: () => translate('Genres'),
type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) {
const genreList = items.reduce((acc, movie) => {
movie.genres.forEach((genre) => {
const genreList = items.reduce((acc, { genres = [] }) => {
genres.forEach((genre) => {
acc.push({
id: genre,
name: genre
@ -487,6 +493,27 @@ export const defaultState = {
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',
label: () => translate('TmdbRating'),

View file

@ -20,7 +20,7 @@ public class GenreSpecification : AutoTaggingSpecificationBase
{
private static readonly GenreSpecificationValidator Validator = new ();
public override int Order => 1;
public override int Order => 2;
public override string ImplementationName => "Genre";
[FieldDefinition(1, Label = "AutoTaggingSpecificationGenre", Type = FieldType.Tag)]

View file

@ -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));
}
}
}

View file

@ -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();
}
}
}

View file

@ -119,6 +119,7 @@
"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.",
"AutoTaggingSpecificationGenre": "Genre(s)",
"AutoTaggingSpecificationKeyword": "Keyword(s)",
"AutoTaggingSpecificationMaximumRuntime": "Maximum Runtime",
"AutoTaggingSpecificationMaximumYear": "Maximum Year",
"AutoTaggingSpecificationMinimumRuntime": "Minimum Runtime",
@ -959,6 +960,7 @@
"KeyboardShortcutsMovieIndexScrollTop": "Movie Index: Scroll Top",
"KeyboardShortcutsOpenModal": "Open This Modal",
"KeyboardShortcutsSaveSettings": "Save Settings",
"Keywords": "Keywords",
"Label": "Label",
"LabelIsRequired": "Label is required",
"Language": "Language",

View file

@ -18,6 +18,7 @@ public class MovieResource
public int? Runtime { get; set; }
public List<ImageResource> Images { get; set; }
public List<string> Genres { get; set; }
public List<string> Keywords { get; set; }
public int Year { get; set; }
public DateTime? Premier { get; set; }

View file

@ -272,7 +272,8 @@ public MovieMetadata MapMovie(MovieResource resource)
movie.Ratings = MapRatings(resource.MovieRatings) ?? new Ratings();
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.Recommendations = resource.Recommendations?.Select(r => r.TmdbId).ToList() ?? new List<int>();

View file

@ -15,6 +15,7 @@ public MovieMetadata()
Translations = new List<MovieTranslation>();
Images = new List<MediaCover.MediaCover>();
Genres = new List<string>();
Keywords = new List<string>();
OriginalLanguage = Language.English;
Recommendations = new List<int>();
Ratings = new Ratings();
@ -24,6 +25,7 @@ public MovieMetadata()
public List<MediaCover.MediaCover> Images { get; set; }
public List<string> Genres { get; set; }
public List<string> Keywords { get; set; }
public DateTime? InCinemas { get; set; }
public DateTime? PhysicalRelease { get; set; }
public DateTime? DigitalRelease { get; set; }

View file

@ -114,6 +114,7 @@ private Movie RefreshMovieInfo(int movieId)
movieMetadata.Runtime = movieInfo.Runtime;
movieMetadata.Ratings = movieInfo.Ratings;
movieMetadata.Genres = movieInfo.Genres;
movieMetadata.Keywords = movieInfo.Keywords;
movieMetadata.Certification = movieInfo.Certification;
movieMetadata.InCinemas = movieInfo.InCinemas;
movieMetadata.Website = movieInfo.Website;

View file

@ -76,6 +76,7 @@ public MovieResource()
public string Folder { get; set; }
public string Certification { get; set; }
public List<string> Genres { get; set; }
public List<string> Keywords { get; set; }
public HashSet<int> Tags { get; set; }
public DateTime Added { 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,
Website = model.MovieMetadata.Value.Website,
Genres = model.MovieMetadata.Value.Genres,
Keywords = model.MovieMetadata.Value.Keywords,
Tags = model.Tags,
Added = model.Added,
AddOptions = model.AddOptions,