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,
|
||||
.digitalRelease,
|
||||
.releaseDate,
|
||||
.genres {
|
||||
.genres,
|
||||
.keywords {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 180px;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ interface CssExports {
|
|||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'keywords': string;
|
||||
'minimumAvailability': string;
|
||||
'movieStatus': string;
|
||||
'originalLanguage': string;
|
||||
|
|
|
|||
|
|
@ -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]}>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@
|
|||
.physicalRelease,
|
||||
.digitalRelease,
|
||||
.releaseDate,
|
||||
.genres {
|
||||
.genres,
|
||||
.keywords {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 180px;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface CssExports {
|
|||
'genres': string;
|
||||
'imdbRating': string;
|
||||
'inCinemas': string;
|
||||
'keywords': string;
|
||||
'minimumAvailability': string;
|
||||
'movieStatus': string;
|
||||
'originalLanguage': string;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ interface Movie extends ModelBase {
|
|||
minimumAvailability: MovieAvailability;
|
||||
path: string;
|
||||
genres: string[];
|
||||
keywords: string[];
|
||||
ratings: Ratings;
|
||||
popularity: number;
|
||||
certification: string;
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue