From 68dfa55b353595c4bd39d5252e4c17f11327baa0 Mon Sep 17 00:00:00 2001 From: Lorenzo Lewis Date: Tue, 1 Oct 2024 19:04:50 +0200 Subject: [PATCH 01/27] Fix typo README.md (#10502) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d30d34ad48..056625b3fd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors) Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available. -Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances. +Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances. ## Major Features Include From c43bd77dae111d6cb2aa70f8805b3be3c64f5f8f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 2 Oct 2024 10:12:07 +0300 Subject: [PATCH 02/27] Display long date tooltips for release dates --- frontend/src/Movie/Details/MovieDetails.js | 15 +++--- .../src/Movie/Details/MovieReleaseDates.tsx | 48 +++++++++++++------ .../Movie/Index/Posters/MovieIndexPoster.tsx | 33 +++++++++++-- .../Index/Posters/MovieIndexPosterInfo.tsx | 33 +++++++++++-- 4 files changed, 101 insertions(+), 28 deletions(-) diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 4e321c0175..33d8c73fe3 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -422,14 +422,15 @@ class MovieDetails extends Component {
{ - !!certification && + certification ? {certification} - + : + null } { - year > 0 && + year > 0 ? - + : + null } { - !!runtime && + runtime ? {formatRuntime(runtime, movieRuntimeFormat)} - + : + null } { diff --git a/frontend/src/Movie/Details/MovieReleaseDates.tsx b/frontend/src/Movie/Details/MovieReleaseDates.tsx index bc5ea0109a..ed5c490ed4 100644 --- a/frontend/src/Movie/Details/MovieReleaseDates.tsx +++ b/frontend/src/Movie/Details/MovieReleaseDates.tsx @@ -2,23 +2,25 @@ import React from 'react'; import { useSelector } from 'react-redux'; import Icon from 'Components/Icon'; import { icons } from 'Helpers/Props'; +import Movie from 'Movie/Movie'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDate from 'Utilities/Date/formatDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import translate from 'Utilities/String/translate'; import styles from './MovieReleaseDates.css'; -interface MovieReleaseDatesProps { - inCinemas?: string; - digitalRelease?: string; - physicalRelease?: string; -} +type MovieReleaseDatesProps = Pick< + Movie, + 'inCinemas' | 'digitalRelease' | 'physicalRelease' +>; -function MovieReleaseDates(props: MovieReleaseDatesProps) { - const { inCinemas, digitalRelease, physicalRelease } = props; - - const { showRelativeDates, shortDateFormat, timeFormat } = useSelector( - createUISettingsSelector() - ); +function MovieReleaseDates({ + inCinemas, + digitalRelease, + physicalRelease, +}: MovieReleaseDatesProps) { + const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } = + useSelector(createUISettingsSelector()); if (!inCinemas && !physicalRelease && !digitalRelease) { return ( @@ -34,10 +36,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) { return ( <> {inCinemas ? ( -
+
+ {getRelativeDate({ date: inCinemas, shortDateFormat, @@ -49,10 +57,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) { ) : null} {digitalRelease ? ( -
+
+ {getRelativeDate({ date: digitalRelease, shortDateFormat, @@ -64,10 +78,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) { ) : null} {physicalRelease ? ( -
+
+ {getRelativeDate({ date: physicalRelease, shortDateFormat, diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx index 02c97ae228..670f30a48f 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx @@ -22,6 +22,7 @@ import { Statistics } from 'Movie/Movie'; import MoviePoster from 'Movie/MoviePoster'; import { executeCommand } from 'Store/Actions/commandActions'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDate from 'Utilities/Date/formatDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import translate from 'Utilities/String/translate'; import createMovieIndexItemSelector from '../createMovieIndexItemSelector'; @@ -243,7 +244,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { ) : null} {showCinemaRelease && inCinemas ? ( -
+
{' '} {getRelativeDate({ date: inCinemas, @@ -256,7 +263,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { ) : null} {showDigitalRelease && digitalRelease ? ( -
+
{' '} {getRelativeDate({ date: digitalRelease, @@ -269,7 +282,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { ) : null} {showPhysicalRelease && physicalRelease ? ( -
+
{' '} {getRelativeDate({ date: physicalRelease, @@ -282,7 +301,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) { ) : null} {showReleaseDate && releaseDate ? ( -
+
{' '} {getRelativeDate({ date: releaseDate, diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPosterInfo.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPosterInfo.tsx index 72269c8cce..42f7c12134 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPosterInfo.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPosterInfo.tsx @@ -9,6 +9,7 @@ import { icons } from 'Helpers/Props'; import Language from 'Language/Language'; import { Ratings } from 'Movie/Movie'; import QualityProfile from 'typings/QualityProfile'; +import formatDate from 'Utilities/Date/formatDate'; import formatDateTime from 'Utilities/Date/formatDateTime'; import getRelativeDate from 'Utilities/Date/getRelativeDate'; import formatBytes from 'Utilities/Number/formatBytes'; @@ -139,7 +140,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) { }); return ( -
+
{inCinemasDate}
); @@ -155,7 +162,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) { }); return ( -
+
{digitalReleaseDate}
); @@ -175,7 +188,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) { }); return ( -
+
{physicalReleaseDate}
); @@ -183,7 +202,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) { if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) { return ( -
+
{' '} {getRelativeDate({ date: releaseDate, From 42fbb790176d731dc04fba0bc672f767f56d1509 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 27 Sep 2024 16:50:42 -0700 Subject: [PATCH 03/27] New: Parse 'BEN THE MAN' release group (cherry picked from commit da610a1f409c9c03cbed1c27ccaedc32f42e636c) --- src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index 14cc70ab24..b0fc6f6f12 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -125,6 +125,7 @@ public void should_parse_release_group(string title, string expected) [TestCase("Movie Title(2023) 1080p SkySHO WEB-DL ESP DD+ 5.1 H.264-EML HDTeam", "EML HDTeam")] [TestCase("Movie Title (2022) BDFull 1080p DTS-HD MA 5.1 AVC LMain", "LMain")] [TestCase("Movie Title (2024) (1080p BluRay x265 SDR DDP 5.1 English - DarQ)", "DarQ")] + [TestCase("Movie Title (2024) (1080p BluRay x265 SDR DDP 5.1 English -BEN THE MAN", "BEN THE MAN")] public void should_parse_exception_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 57612e4370..6fdf822b73 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -159,7 +159,7 @@ public static class Parser // Handle Exception Release Groups that don't follow -RlsGrp; Manual List // name only...BE VERY CAREFUL WITH THIS, HIGH CHANCE OF FALSE POSITIVES - private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"\b(?KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon|ZØNEHD|Tigole|HQMUX|VARYG|YIFY|YTS(.(MX|LT|AG))?|TMd|Eml HDTeam|LMain|DarQ)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"\b(?KRaLiMaRKo|E\.N\.D|D\-Z0N3|Koten_Gars|BluDragon|ZØNEHD|Tigole|HQMUX|VARYG|YIFY|YTS(.(MX|LT|AG))?|TMd|Eml HDTeam|LMain|DarQ|BEN THE MAN)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|'|\|)+", RegexOptions.Compiled); private static readonly Regex SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", RegexOptions.Compiled); From ac767ed386cc2be0dcd74d7cc9289939f3a00b0a Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 2 Oct 2024 17:25:46 +0300 Subject: [PATCH 04/27] New: Add 'Movie CleanTitleThe' token First attempt to fix movie folder validation by ignoring invalid tokens mixed from 'Original' and 'The' Co-authored-by: Stevie Robinson --- .../MediaManagement/Naming/NamingModal.js | 2 + .../CleanTitleTheFixture.cs | 78 +++++++++++++++++++ .../Organizer/FileNameBuilder.cs | 14 +++- 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 1d3f593862..9917c2caab 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -75,7 +75,9 @@ const movieTokens = [ { token: '{Movie Title}', example: 'Movie\'s Title', footNote: 1 }, { token: '{Movie Title:DE}', example: 'Titel des Films', footNote: 1 }, { token: '{Movie CleanTitle}', example: 'Movies Title', footNote: 1 }, + { token: '{Movie CleanTitle:DE}', example: 'Titel des Films', footNote: 1 }, { token: '{Movie TitleThe}', example: 'Movie\'s Title, The', footNote: 1 }, + { token: '{Movie CleanTitleThe}', example: 'Movies Title, The', footNote: 1 }, { token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 }, { token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 }, { token: '{Movie TitleFirstCharacter}', example: 'M' }, diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs new file mode 100644 index 0000000000..9a2a2b7cc0 --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CleanTitleTheFixture.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.CustomFormats; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + public class CleanTitleTheFixture : CoreTest + { + private Movie _movie; + private MovieFile _movieFile; + private NamingConfig _namingConfig; + + [SetUp] + public void Setup() + { + _movie = Builder + .CreateNew() + .Build(); + + _movieFile = new MovieFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "RadarrTest" }; + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameMovies = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); + } + + [TestCase("The Mist", "Mist, The")] + [TestCase("A Place to Call Home", "Place to Call Home, A")] + [TestCase("An Adventure in Space and Time", "Adventure in Space and Time, An")] + [TestCase("The Flash (2010)", "Flash, The 2010")] + [TestCase("A League Of Their Own (AU)", "League Of Their Own, A AU")] + [TestCase("The Fixer (ZH) (2015)", "Fixer, The ZH 2015")] + [TestCase("The Sixth Sense 2 (Thai)", "Sixth Sense 2, The Thai")] + [TestCase("The Amazing Race (Latin America)", "Amazing Race, The Latin America")] + [TestCase("The Rat Pack (A&E)", "Rat Pack, The AandE")] + [TestCase("The Climax: I (Almost) Got Away With It (2016)", "Climax I Almost Got Away With It, The 2016")] + public void should_get_expected_title_back(string title, string expected) + { + _movie.Title = title; + _namingConfig.StandardMovieFormat = "{Movie CleanTitleThe}"; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(expected); + } + + [TestCase("A")] + [TestCase("Anne")] + [TestCase("Theodore")] + [TestCase("3%")] + public void should_not_change_title(string title) + { + _movie.Title = title; + _namingConfig.StandardMovieFormat = "{Movie CleanTitleThe}"; + + Subject.BuildFileName(_movie, _movieFile) + .Should().Be(title); + } + } +} diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index b5d4370c75..f726a9349a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -41,7 +41,7 @@ public class FileNameBuilder : IBuildFileNames private static readonly Regex TitleRegex = new Regex(@"(?\{(?:imdb-|edition-))?\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[ ,a-z0-9|+-]+(?[-} ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - public static readonly Regex MovieTitleRegex = new Regex(@"(?\{((?:(Movie|Original))(?[- ._])(Clean)?(Original)?(Title|Filename)(The)?)(?::(?[a-z0-9|-]+))?\})", + public static readonly Regex MovieTitleRegex = new Regex(@"(?\{(?:(?:Movie)(?[- ._])(?:Clean)?(?:OriginalTitle|Title(?:The)?)(?::(?[a-z0-9|-]+))?|Original[- ._](?:Title|Filename))\})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex FileNameCleanupRegex = new Regex(@"([- ._])(\1)+", RegexOptions.Compiled); @@ -226,6 +226,17 @@ public static string TitleThe(string title) return TitlePrefixRegex.Replace(title, "$2, $1$3"); } + public static string CleanTitleThe(string title) + { + if (TitlePrefixRegex.IsMatch(title)) + { + var splitResult = TitlePrefixRegex.Split(title); + return $"{CleanTitle(splitResult[2]).Trim()}, {splitResult[1]}{CleanTitle(splitResult[3])}"; + } + + return CleanTitle(title); + } + public static string TitleFirstCharacter(string title) { if (char.IsLetterOrDigit(title[0])) @@ -260,6 +271,7 @@ private void AddMovieTokens(Dictionary> tokenHa tokenHandlers["{Movie Title}"] = m => Truncate(GetLanguageTitle(movie, m.CustomFormat), m.CustomFormat); tokenHandlers["{Movie CleanTitle}"] = m => Truncate(CleanTitle(GetLanguageTitle(movie, m.CustomFormat)), m.CustomFormat); tokenHandlers["{Movie TitleThe}"] = m => Truncate(TitleThe(movie.Title), m.CustomFormat); + tokenHandlers["{Movie CleanTitleThe}"] = m => Truncate(CleanTitleThe(movie.Title), m.CustomFormat); tokenHandlers["{Movie TitleFirstCharacter}"] = m => TitleFirstCharacter(TitleThe(GetLanguageTitle(movie, m.CustomFormat))); tokenHandlers["{Movie OriginalTitle}"] = m => Truncate(movie.MovieMetadata.Value.OriginalTitle, m.CustomFormat) ?? string.Empty; tokenHandlers["{Movie CleanOriginalTitle}"] = m => Truncate(CleanTitle(movie.MovieMetadata.Value.OriginalTitle ?? string.Empty), m.CustomFormat); From 6e04dc894b2ba10c1bec41e63efebe3c9b4efd28 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 2 Oct 2024 18:32:15 +0300 Subject: [PATCH 05/27] Fixed: Validate path on movie update --- src/Radarr.Api.V3/Movies/MovieController.cs | 44 ++++++++++++--------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/Radarr.Api.V3/Movies/MovieController.cs b/src/Radarr.Api.V3/Movies/MovieController.cs index 884943a7c6..dc3189a190 100644 --- a/src/Radarr.Api.V3/Movies/MovieController.cs +++ b/src/Radarr.Api.V3/Movies/MovieController.cs @@ -86,27 +86,35 @@ public MovieController(IBroadcastSignalRMessage signalRBroadcaster, _rootFolderService = rootFolderService; _logger = logger; - SharedValidator.RuleFor(s => s.QualityProfileId).ValidId(); + SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(moviesPathValidator) + .SetValidator(moviesAncestorValidator) + .SetValidator(recycleBinValidator) + .SetValidator(systemFolderValidator) + .When(s => s.Path.IsNotNullOrWhiteSpace()); - SharedValidator.RuleFor(s => s.Path) - .Cascade(CascadeMode.Stop) - .IsValidPath() - .SetValidator(rootFolderValidator) - .SetValidator(mappedNetworkDriveValidator) - .SetValidator(moviesPathValidator) - .SetValidator(moviesAncestorValidator) - .SetValidator(recycleBinValidator) - .SetValidator(systemFolderValidator) - .When(s => !s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath() + .SetValidator(rootFolderExistsValidator) + .SetValidator(movieFolderAsRootFolderValidator) + .When(s => s.Path.IsNullOrWhiteSpace()); - SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator); + PutValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop) + .NotEmpty() + .IsValidPath(); + + SharedValidator.RuleFor(s => s.QualityProfileId).Cascade(CascadeMode.Stop) + .ValidId() + .SetValidator(qualityProfileExistsValidator); - PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); - PostValidator.RuleFor(s => s.RootFolderPath) - .IsValidPath() - .SetValidator(rootFolderExistsValidator) - .SetValidator(movieFolderAsRootFolderValidator) - .When(s => s.Path.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Title).NotEmpty().When(s => s.TmdbId <= 0); PostValidator.RuleFor(s => s.TmdbId).NotNull().NotEmpty().SetValidator(moviesExistsValidator); } From 40551ba5a32ecca5801c4ca70f78eb24fdc45383 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 2 Oct 2024 22:32:33 +0300 Subject: [PATCH 06/27] Fixed: Custom filters with release date filter Fixes #10508 --- frontend/src/Store/Actions/movieActions.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/Store/Actions/movieActions.js b/frontend/src/Store/Actions/movieActions.js index 7b3826858c..cd56135894 100644 --- a/frontend/src/Store/Actions/movieActions.js +++ b/frontend/src/Store/Actions/movieActions.js @@ -156,6 +156,10 @@ export const filterPredicates = { return dateFilterPredicate(item.digitalRelease, filterValue, type); }, + releaseDate: function(item, filterValue, type) { + return dateFilterPredicate(item.releaseDate, filterValue, type); + }, + tmdbRating: function({ ratings = {} }, filterValue, type) { const predicate = filterTypePredicates[type]; From 9a5f4bef63133cd0be672fbf1511175b43e796d5 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 3 Oct 2024 15:04:00 +0300 Subject: [PATCH 07/27] Check if root folder is not empty on files import --- .../MoveMovieFileFixture.cs | 9 +++++---- .../MediaFiles/MovieFileMovingService.cs | 14 +++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/MovieFileMovingServiceTests/MoveMovieFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/MovieFileMovingServiceTests/MoveMovieFileFixture.cs index 4af1d784cd..d93dae7d76 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/MovieFileMovingServiceTests/MoveMovieFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/MovieFileMovingServiceTests/MoveMovieFileFixture.cs @@ -48,6 +48,11 @@ public void Setup() .Returns(@"C:\Test\Movies\Movie\File Name.avi".AsOsAgnostic()); var rootFolder = @"C:\Test\Movies\".AsOsAgnostic(); + + Mocker.GetMock() + .Setup(s => s.GetBestRootFolderPath(It.IsAny(), null)) + .Returns(rootFolder); + Mocker.GetMock() .Setup(s => s.FolderExists(rootFolder)) .Returns(true); @@ -55,10 +60,6 @@ public void Setup() Mocker.GetMock() .Setup(s => s.FileExists(It.IsAny())) .Returns(true); - - Mocker.GetMock() - .Setup(s => s.GetBestRootFolderPath(It.IsAny(), null)) - .Returns(rootFolder); } [Test] diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index b198697fd9..20e91836ec 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -30,9 +30,9 @@ public class MovieFileMovingService : IMoveMovieFiles private readonly IDiskProvider _diskProvider; private readonly IMediaFileAttributeService _mediaFileAttributeService; private readonly IImportScript _scriptImportDecider; + private readonly IRootFolderService _rootFolderService; private readonly IEventAggregator _eventAggregator; private readonly IConfigService _configService; - private readonly IRootFolderService _rootFolderService; private readonly Logger _logger; public MovieFileMovingService(IUpdateMovieFileService updateMovieFileService, @@ -41,9 +41,9 @@ public MovieFileMovingService(IUpdateMovieFileService updateMovieFileService, IDiskProvider diskProvider, IMediaFileAttributeService mediaFileAttributeService, IImportScript scriptImportDecider, + IRootFolderService rootFolderService, IEventAggregator eventAggregator, IConfigService configService, - IRootFolderService rootFolderService, Logger logger) { _updateMovieFileService = updateMovieFileService; @@ -52,9 +52,9 @@ public MovieFileMovingService(IUpdateMovieFileService updateMovieFileService, _diskProvider = diskProvider; _mediaFileAttributeService = mediaFileAttributeService; _scriptImportDecider = scriptImportDecider; + _rootFolderService = rootFolderService; _eventAggregator = eventAggregator; _configService = configService; - _rootFolderService = rootFolderService; _logger = logger; } @@ -167,13 +167,17 @@ private void EnsureMovieFolder(MovieFile movieFile, LocalMovie localMovie, strin private void EnsureMovieFolder(MovieFile movieFile, Movie movie, string filePath) { var movieFileFolder = Path.GetDirectoryName(filePath); - var movieFolder = movie.Path; var rootFolder = _rootFolderService.GetBestRootFolderPath(movieFolder); + if (rootFolder.IsNullOrWhiteSpace()) + { + throw new RootFolderNotFoundException($"Root folder was not found, '{movieFolder}' is not a subdirectory of a defined root folder."); + } + if (!_diskProvider.FolderExists(rootFolder)) { - throw new RootFolderNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + throw new RootFolderNotFoundException($"Root folder '{rootFolder}' was not found."); } var changed = false; From d37e71415fce9b0434b7679ff4e07a40c0d22e9f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Sep 2024 08:52:32 +0300 Subject: [PATCH 08/27] Convert Release Profiles to TypeScript --- frontend/src/App/State/SettingsAppState.ts | 8 + .../src/Components/Form/FormInputGroup.js | 4 + frontend/src/Settings/Profiles/Profiles.js | 4 +- .../Release/EditReleaseProfileModal.js | 27 --- .../Release/EditReleaseProfileModal.tsx | 41 ++++ .../EditReleaseProfileModalConnector.js | 39 ---- ....js => EditReleaseProfileModalContent.tsx} | 169 ++++++++++----- ...EditReleaseProfileModalContentConnector.js | 112 ---------- .../Profiles/Release/ReleaseProfile.js | 197 ------------------ ...leaseProfile.css => ReleaseProfileRow.css} | 0 ...le.css.d.ts => ReleaseProfileRow.css.d.ts} | 0 .../Profiles/Release/ReleaseProfileRow.tsx | 130 ++++++++++++ .../Profiles/Release/ReleaseProfiles.css | 2 +- .../Profiles/Release/ReleaseProfiles.js | 102 --------- .../Profiles/Release/ReleaseProfiles.tsx | 81 +++++++ .../Release/ReleaseProfilesConnector.js | 74 ------- .../src/typings/Settings/ReleaseProfile.ts | 12 ++ src/NzbDrone.Core/Localization/Core/en.json | 2 +- 18 files changed, 393 insertions(+), 611 deletions(-) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js rename frontend/src/Settings/Profiles/Release/{EditReleaseProfileModalContent.js => EditReleaseProfileModalContent.tsx} (51%) delete mode 100644 frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfile.js rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css => ReleaseProfileRow.css} (100%) rename frontend/src/Settings/Profiles/Release/{ReleaseProfile.css.d.ts => ReleaseProfileRow.css.d.ts} (100%) create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.js create mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx delete mode 100644 frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js create mode 100644 frontend/src/typings/Settings/ReleaseProfile.ts diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 5d259a7f8c..9f3c03abf8 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -16,6 +16,7 @@ import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import General from 'typings/Settings/General'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; export interface DownloadClientAppState @@ -49,6 +50,12 @@ export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} +export interface ReleaseProfilesAppState + extends AppSectionState, + AppSectionSaveState { + pendingChanges: Partial; +} + export interface CustomFormatAppState extends AppSectionState, AppSectionDeleteState, @@ -83,6 +90,7 @@ interface SettingsAppState { languages: LanguageSettingsAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; + releaseProfiles: ReleaseProfilesAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 537af8b530..39a5c9a214 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -272,6 +272,8 @@ FormInputGroup.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.any, values: PropTypes.arrayOf(PropTypes.any), + placeholder: PropTypes.string, + delimiters: PropTypes.arrayOf(PropTypes.string), isDisabled: PropTypes.bool, type: PropTypes.string.isRequired, kind: PropTypes.oneOf(kinds.all), @@ -284,8 +286,10 @@ FormInputGroup.propTypes = { helpTextWarning: PropTypes.string, helpLink: PropTypes.string, autoFocus: PropTypes.bool, + canEdit: PropTypes.bool, includeNoChange: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool, + includeAny: PropTypes.bool, selectedValueOptions: PropTypes.object, indexerFlags: PropTypes.number, pending: PropTypes.bool, diff --git a/frontend/src/Settings/Profiles/Profiles.js b/frontend/src/Settings/Profiles/Profiles.js index 330591ed64..e54c6fdbdb 100644 --- a/frontend/src/Settings/Profiles/Profiles.js +++ b/frontend/src/Settings/Profiles/Profiles.js @@ -7,7 +7,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import DelayProfilesConnector from './Delay/DelayProfilesConnector'; import QualityProfilesConnector from './Quality/QualityProfilesConnector'; -import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector'; +import ReleaseProfiles from './Release/ReleaseProfiles'; // Only a single DragDrop Context can exist so it's done here to allow editing // quality profiles and reordering delay profiles to work. @@ -26,7 +26,7 @@ class Profiles extends Component { - + diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js deleted file mode 100644 index a948ab1235..0000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.js +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import { sizes } from 'Helpers/Props'; -import EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector'; - -function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditReleaseProfileModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx new file mode 100644 index 0000000000..d4ecfc59cc --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; + +interface EditReleaseProfileModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModal({ + isOpen, + onModalClose, + ...otherProps +}: EditReleaseProfileModalProps) { + const dispatch = useDispatch(); + + const onModalClosePress = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.releaseProfiles', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditReleaseProfileModal; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js deleted file mode 100644 index e846ff6ffc..0000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditReleaseProfileModal from './EditReleaseProfileModal'; - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditReleaseProfileModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.releaseProfiles' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx similarity index 51% rename from frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js rename to frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index bdd9777250..9f672ea4a5 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -1,5 +1,7 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; import FormInputGroup from 'Components/Form/FormInputGroup'; @@ -10,33 +12,102 @@ import ModalBody from 'Components/Modal/ModalBody'; import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; +import usePrevious from 'Helpers/Hooks/usePrevious'; import { inputTypes, kinds } from 'Helpers/Props'; +import { + saveReleaseProfile, + setReleaseProfileValue, +} from 'Store/Actions/Settings/releaseProfiles'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { PendingSection } from 'typings/pending'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import translate from 'Utilities/String/translate'; import styles from './EditReleaseProfileModalContent.css'; const tagInputDelimiters = ['Tab', 'Enter']; -function EditReleaseProfileModalContent(props) { - const { - isSaving, - saveError, - item, - onInputChange, - onModalClose, - onSavePress, - onDeleteReleaseProfilePress, - ...otherProps - } = props; +const newReleaseProfile = { + enabled: true, + required: [], + ignored: [], + tags: [], + indexerId: 0, +}; - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId - } = item; +function createReleaseProfileSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.releaseProfiles, + (releaseProfiles) => { + const { items, isFetching, error, isSaving, saveError, pendingChanges } = + releaseProfiles; + + const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +interface EditReleaseProfileModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteReleaseProfilePress?: () => void; +} + +function EditReleaseProfileModalContent( + props: EditReleaseProfileModalContentProps +) { + const { id, onModalClose, onDeleteReleaseProfilePress } = props; + + const { item, isFetching, isSaving, error, saveError, ...otherProps } = + useSelector(createReleaseProfileSelector(id)); + + const { name, enabled, required, ignored, tags, indexerId } = item; + + const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); + + useEffect(() => { + if (!id) { + Object.keys(newReleaseProfile).forEach((name) => { + dispatch( + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + setReleaseProfileValue({ + name, + value: newReleaseProfile[name as keyof typeof newReleaseProfile], + }) + ); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }); + + const handleSavePress = useCallback(() => { + dispatch(saveReleaseProfile({ id })); + }, [dispatch, id]); + + const handleInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue(payload)); + }, + [dispatch] + ); return ( @@ -46,7 +117,6 @@ function EditReleaseProfileModalContent(props) {
- {translate('Name')} @@ -56,7 +126,7 @@ function EditReleaseProfileModalContent(props) { {...name} placeholder={translate('OptionalName')} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -68,7 +138,7 @@ function EditReleaseProfileModalContent(props) { name="enabled" helpText={translate('EnableProfileHelpText')} {...enabled} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -85,7 +155,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -102,7 +172,7 @@ function EditReleaseProfileModalContent(props) { placeholder={translate('AddNewRestriction')} delimiters={tagInputDelimiters} canEdit={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -113,10 +183,12 @@ function EditReleaseProfileModalContent(props) { type={inputTypes.INDEXER_SELECT} name="indexerId" helpText={translate('ReleaseProfileIndexerHelpText')} - helpTextWarning={translate('ReleaseProfileIndexerHelpTextWarning')} + helpTextWarning={translate( + 'ReleaseProfileIndexerHelpTextWarning' + )} {...indexerId} includeAny={true} - onChange={onInputChange} + onChange={handleInputChange} /> @@ -128,33 +200,28 @@ function EditReleaseProfileModalContent(props) { name="tags" helpText={translate('ReleaseProfileTagMovieHelpText')} {...tags} - onChange={onInputChange} + onChange={handleInputChange} />
- { - id && - - } + {id ? ( + + ) : null} - + {translate('Save')} @@ -163,14 +230,4 @@ function EditReleaseProfileModalContent(props) { ); } -EditReleaseProfileModalContent.propTypes = { - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onDeleteReleaseProfilePress: PropTypes.func -}; - export default EditReleaseProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js deleted file mode 100644 index 0371a1a7ad..0000000000 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContentConnector.js +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditReleaseProfileModalContent from './EditReleaseProfileModalContent'; - -const newReleaseProfile = { - enabled: true, - required: [], - ignored: [], - tags: [], - indexerId: 0 -}; - -function createMapStateToProps() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.releaseProfiles, - (id, releaseProfiles) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = releaseProfiles; - - const profile = id ? items.find((i) => i.id === id) : newReleaseProfile; - const settings = selectSettings(profile, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -const mapDispatchToProps = { - setReleaseProfileValue, - saveReleaseProfile -}; - -class EditReleaseProfileModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newReleaseProfile).forEach((name) => { - this.props.setReleaseProfileValue({ - name, - value: newReleaseProfile[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setReleaseProfileValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveReleaseProfile({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditReleaseProfileModalContentConnector.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setReleaseProfileValue: PropTypes.func.isRequired, - saveReleaseProfile: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js b/frontend/src/Settings/Profiles/Release/ReleaseProfile.js deleted file mode 100644 index cc4017d8d2..0000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfile.js +++ /dev/null @@ -1,197 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import Label from 'Components/Label'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TagList from 'Components/TagList'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import styles from './ReleaseProfile.css'; - -class ReleaseProfile extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onEditReleaseProfilePress = () => { - this.setState({ isEditReleaseProfileModalOpen: true }); - }; - - onEditReleaseProfileModalClose = () => { - this.setState({ isEditReleaseProfileModalOpen: false }); - }; - - onDeleteReleaseProfilePress = () => { - this.setState({ - isEditReleaseProfileModalOpen: false, - isDeleteReleaseProfileModalOpen: true - }); - }; - - onDeleteReleaseProfileModalClose = () => { - this.setState({ isDeleteReleaseProfileModalOpen: false }); - }; - - onConfirmDeleteReleaseProfile = () => { - this.props.onConfirmDeleteReleaseProfile(this.props.id); - }; - - // - // Render - - render() { - const { - id, - name, - enabled, - required, - ignored, - tags, - indexerId, - tagList, - indexerList - } = this.props; - - const { - isEditReleaseProfileModalOpen, - isDeleteReleaseProfileModalOpen - } = this.state; - - const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId); - - return ( - - { - name ? -
- {name} -
: - null - } - -
- { - required.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- -
- { - ignored.map((item) => { - if (!item) { - return null; - } - - return ( - - ); - }) - } -
- - - -
- { - !enabled && - - } - - { - indexer && - - } -
- - - - -
- ); - } -} - -ReleaseProfile.propTypes = { - id: PropTypes.number.isRequired, - name: PropTypes.string, - enabled: PropTypes.bool.isRequired, - required: PropTypes.arrayOf(PropTypes.string).isRequired, - ignored: PropTypes.arrayOf(PropTypes.string).isRequired, - tags: PropTypes.arrayOf(PropTypes.number).isRequired, - indexerId: PropTypes.number.isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -ReleaseProfile.defaultProps = { - enabled: true, - required: [], - ignored: [], - indexerId: 0 -}; - -export default ReleaseProfile; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts similarity index 100% rename from frontend/src/Settings/Profiles/Release/ReleaseProfile.css.d.ts rename to frontend/src/Settings/Profiles/Release/ReleaseProfileRow.css.d.ts diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx new file mode 100644 index 0000000000..c2ad9f4b8f --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx @@ -0,0 +1,130 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Tag } from 'App/State/TagsAppState'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagList from 'Components/TagList'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { kinds } from 'Helpers/Props'; +import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles'; +import Indexer from 'typings/Indexer'; +import ReleaseProfile from 'typings/Settings/ReleaseProfile'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import styles from './ReleaseProfileRow.css'; + +interface ReleaseProfileProps extends ReleaseProfile { + tagList: Tag[]; + indexerList: Indexer[]; +} + +function ReleaseProfileRow(props: ReleaseProfileProps) { + const { + id, + name, + enabled = true, + required = [], + ignored = [], + tags, + indexerId = 0, + tagList, + indexerList, + } = props; + + const dispatch = useDispatch(); + + const [ + isEditReleaseProfileModalOpen, + setEditReleaseProfileModalOpen, + setEditReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalOpen, + setDeleteReleaseProfileModalClosed, + ] = useModalOpenState(false); + + const handleDeletePress = useCallback(() => { + dispatch(deleteReleaseProfile({ id })); + }, [id, dispatch]); + + const indexer = + indexerId !== 0 && indexerList.find((i) => i.id === indexerId); + + return ( + + {name ?
{name}
: null} + +
+ {required.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ +
+ {ignored.map((item) => { + if (!item) { + return null; + } + + return ( + + ); + })} +
+ + + +
+ {enabled ? null : ( + + )} + + {indexer ? ( + + ) : null} +
+ + + + +
+ ); +} + +export default ReleaseProfileRow; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css index 9e9715e770..43f17b9dc7 100644 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.css @@ -4,7 +4,7 @@ } .addReleaseProfile { - composes: releaseProfile from '~./ReleaseProfile.css'; + composes: releaseProfile from '~./ReleaseProfileRow.css'; background-color: var(--cardAlternateBackgroundColor); color: var(--gray); diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js deleted file mode 100644 index 51aa57b736..0000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.js +++ /dev/null @@ -1,102 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Card from 'Components/Card'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector'; -import ReleaseProfile from './ReleaseProfile'; -import styles from './ReleaseProfiles.css'; - -class ReleaseProfiles extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddReleaseProfileModalOpen: false - }; - } - - // - // Listeners - - onAddReleaseProfilePress = () => { - this.setState({ isAddReleaseProfileModalOpen: true }); - }; - - onAddReleaseProfileModalClose = () => { - this.setState({ isAddReleaseProfileModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - tagList, - indexerList, - onConfirmDeleteReleaseProfile, - ...otherProps - } = this.props; - - return ( -
- -
- -
- -
-
- - { - items.map((item) => { - return ( - - ); - }) - } -
- - -
-
- ); - } -} - -ReleaseProfiles.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - tagList: PropTypes.arrayOf(PropTypes.object).isRequired, - indexerList: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteReleaseProfile: PropTypes.func.isRequired -}; - -export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx new file mode 100644 index 0000000000..bf0b47a586 --- /dev/null +++ b/frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import { ReleaseProfilesAppState } from 'App/State/SettingsAppState'; +import Card from 'Components/Card'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import ReleaseProfileRow from 'Settings/Profiles/Release/ReleaseProfileRow'; +import { fetchIndexers } from 'Store/Actions/Settings/indexers'; +import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import translate from 'Utilities/String/translate'; +import EditReleaseProfileModal from './EditReleaseProfileModal'; +import styles from './ReleaseProfiles.css'; + +function ReleaseProfiles() { + const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState = + useSelector(createClientSideCollectionSelector('settings.releaseProfiles')); + + const tagList = useSelector(createTagsSelector()); + const indexerList = useSelector( + (state: AppState) => state.settings.indexers.items + ); + + const dispatch = useDispatch(); + + const [ + isAddReleaseProfileModalOpen, + setAddReleaseProfileModalOpen, + setAddReleaseProfileModalClosed, + ] = useModalOpenState(false); + + useEffect(() => { + dispatch(fetchReleaseProfiles()); + dispatch(fetchIndexers()); + }, [dispatch]); + + return ( +
+ +
+ +
+ +
+
+ + {items.map((item) => { + return ( + + ); + })} +
+ + +
+
+ ); +} + +export default ReleaseProfiles; diff --git a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js b/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js deleted file mode 100644 index ad254b2dfc..0000000000 --- a/frontend/src/Settings/Profiles/Release/ReleaseProfilesConnector.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; -import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import ReleaseProfiles from './ReleaseProfiles'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.releaseProfiles, - (state) => state.settings.indexers, - createTagsSelector(), - (releaseProfiles, indexers, tagList) => { - return { - ...releaseProfiles, - tagList, - isIndexersPopulated: indexers.isPopulated, - indexerList: indexers.items - }; - } - ); -} - -const mapDispatchToProps = { - fetchIndexers, - fetchReleaseProfiles, - deleteReleaseProfile -}; - -class ReleaseProfilesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.isPopulated) { - this.props.fetchReleaseProfiles(); - } - - if (!this.props.isIndexersPopulated) { - this.props.fetchIndexers(); - } - } - - // - // Listeners - - onConfirmDeleteReleaseProfile = (id) => { - this.props.deleteReleaseProfile({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ReleaseProfilesConnector.propTypes = { - isPopulated: PropTypes.bool.isRequired, - isIndexersPopulated: PropTypes.bool.isRequired, - fetchReleaseProfiles: PropTypes.func.isRequired, - deleteReleaseProfile: PropTypes.func.isRequired, - fetchIndexers: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector); diff --git a/frontend/src/typings/Settings/ReleaseProfile.ts b/frontend/src/typings/Settings/ReleaseProfile.ts new file mode 100644 index 0000000000..847e7d54ec --- /dev/null +++ b/frontend/src/typings/Settings/ReleaseProfile.ts @@ -0,0 +1,12 @@ +import ModelBase from 'App/ModelBase'; + +interface ReleaseProfile extends ModelBase { + name: string; + enabled: boolean; + required: string[]; + ignored: string[]; + indexerId: number; + tags: number[]; +} + +export default ReleaseProfile; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 884c7394ce..a3beafbbd7 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -336,7 +336,7 @@ "DeleteQualityProfile": "Delete Quality Profile", "DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?", "DeleteReleaseProfile": "Delete Release Profile", - "DeleteReleaseProfileMessageText": "Are you sure you want to delete this release profile '{name}'?", + "DeleteReleaseProfileMessageText": "Are you sure you want to delete the release profile '{name}'?", "DeleteRemotePathMapping": "Delete Remote Path Mapping", "DeleteRemotePathMappingMessageText": "Are you sure you want to delete this remote path mapping?", "DeleteRestriction": "Delete Restriction", From c9836f997cc492c15e035fe74b90d4bf2b321273 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 4 Oct 2024 12:40:37 +0300 Subject: [PATCH 09/27] Fixed: Clean paths for top level root folders --- .../PathExtensionFixture.cs | 21 +++++++++++++++++++ src/NzbDrone.Common/Disk/OsPath.cs | 14 +++++++++++-- .../Extensions/PathExtensions.cs | 8 ++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index f0a42b0cd1..13f28b681d 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -392,5 +392,26 @@ public void IsPathValid_should_be_false_on_unix(string path) PosixOnly(); path.AsOsAgnostic().IsPathValid(PathValidationType.CurrentOs).Should().BeFalse(); } + + [TestCase(@"C:\", @"C:\")] + [TestCase(@"C:\\", @"C:\")] + [TestCase(@"C:\Test", @"C:\Test")] + [TestCase(@"C:\Test\", @"C:\Test")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\", @"\\server\share")] + public void windows_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } + + [TestCase("/", "/")] + [TestCase("//", "/")] + [TestCase("/test", "/test")] + [TestCase("/test/", "/test")] + [TestCase("/test//", "/test")] + public void unix_path_should_return_clean_path(string path, string cleanPath) + { + path.GetCleanPath().Should().Be(cleanPath); + } } } diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index 45e5207612..42fdaf567b 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -104,9 +104,19 @@ private static string TrimTrailingSlash(string path, OsPathKind kind) switch (kind) { case OsPathKind.Windows when !path.EndsWith(":\\"): - return path.TrimEnd('\\'); + while (!path.EndsWith(":\\") && path.EndsWith('\\')) + { + path = path[..^1]; + } + + return path; case OsPathKind.Unix when path != "/": - return path.TrimEnd('/'); + while (path != "/" && path.EndsWith('/')) + { + path = path[..^1]; + } + + return path; } return path; diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 2424323fc0..865e25138f 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -26,8 +26,6 @@ public static class PathExtensions private static readonly string UPDATE_CLIENT_FOLDER_NAME = "Radarr.Update" + Path.DirectorySeparatorChar; private static readonly string UPDATE_LOG_FOLDER_NAME = "UpdateLogs" + Path.DirectorySeparatorChar; - private static readonly Regex PARENT_PATH_END_SLASH_REGEX = new Regex(@"(? Date: Fri, 4 Oct 2024 12:55:41 +0300 Subject: [PATCH 10/27] Fixed: Cleaning the path for movie collections with top level folders --- src/NzbDrone.Core/Movies/RefreshMovieService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Movies/RefreshMovieService.cs b/src/NzbDrone.Core/Movies/RefreshMovieService.cs index d919a495e7..f3630c0a39 100644 --- a/src/NzbDrone.Core/Movies/RefreshMovieService.cs +++ b/src/NzbDrone.Core/Movies/RefreshMovieService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.AutoTagging; using NzbDrone.Core.Configuration; @@ -145,7 +146,7 @@ private Movie RefreshMovieInfo(int movieId) SearchOnAdd = movie.AddOptions?.SearchForMovie ?? false, QualityProfileId = movie.QualityProfileId, MinimumAvailability = movie.MinimumAvailability, - RootFolderPath = _folderService.GetBestRootFolderPath(movie.Path).TrimEnd('/', '\\', ' '), + RootFolderPath = _folderService.GetBestRootFolderPath(movie.Path).GetCleanPath(), Tags = movie.Tags }); From cfdb7a15de183f19b8496e5ec728a63362194ca7 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 5 Oct 2024 12:05:18 +0300 Subject: [PATCH 11/27] Simplify defaults set when adding release profiles and list exclusions --- .../EditImportListExclusionModal.tsx | 6 +-- .../EditImportListExclusionModalContent.tsx | 42 +++++++++---------- .../Release/EditReleaseProfileModal.tsx | 6 +-- .../EditReleaseProfileModalContent.tsx | 23 ++++------ 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx index b889a81050..7f5feafabb 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx @@ -19,7 +19,7 @@ function EditImportListExclusionModal( const dispatch = useDispatch(); - const onModalClosePress = useCallback(() => { + const handleModalClose = useCallback(() => { dispatch( clearPendingChanges({ section: 'settings.importListExclusions', @@ -29,10 +29,10 @@ function EditImportListExclusionModal( }, [dispatch, onModalClose]); return ( - + ); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx index d1aa0852d8..766f883c93 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -32,12 +32,6 @@ const newImportListExclusion = { tmdbId: 0, }; -interface EditImportListExclusionModalContentProps { - id?: number; - onModalClose: () => void; - onDeleteImportListExclusionPress?: () => void; -} - function createImportListExclusionSelector(id?: number) { return createSelector( (state: AppState) => state.settings.importListExclusions, @@ -63,12 +57,24 @@ function createImportListExclusionSelector(id?: number) { ); } -function EditImportListExclusionModalContent( - props: EditImportListExclusionModalContentProps -) { - const { id, onModalClose, onDeleteImportListExclusionPress } = props; +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModalContent({ + id, + onModalClose, + onDeleteImportListExclusionPress, +}: EditImportListExclusionModalContentProps) { + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(id)); + + const { movieTitle, movieYear, tmdbId } = item; const dispatch = useDispatch(); + const previousIsSaving = usePrevious(isSaving); const dispatchSetImportListExclusionValue = (payload: { name: string; @@ -78,20 +84,10 @@ function EditImportListExclusionModalContent( dispatch(setImportListExclusionValue(payload)); }; - const { isFetching, isSaving, item, error, saveError, ...otherProps } = - useSelector(createImportListExclusionSelector(props.id)); - const previousIsSaving = usePrevious(isSaving); - - const { movieTitle, movieYear, tmdbId } = item; - useEffect(() => { if (!id) { - Object.keys(newImportListExclusion).forEach((name) => { - dispatchSetImportListExclusionValue({ - name, - value: - newImportListExclusion[name as keyof typeof newImportListExclusion], - }); + Object.entries(newImportListExclusion).forEach(([name, value]) => { + dispatchSetImportListExclusionValue({ name, value }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -101,7 +97,7 @@ function EditImportListExclusionModalContent( if (previousIsSaving && !isSaving && !saveError) { onModalClose(); } - }); + }, [previousIsSaving, isSaving, saveError, onModalClose]); const onSavePress = useCallback(() => { dispatch(saveImportListExclusion({ id })); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx index d4ecfc59cc..cb7c2cef15 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModal.tsx @@ -19,7 +19,7 @@ function EditReleaseProfileModal({ }: EditReleaseProfileModalProps) { const dispatch = useDispatch(); - const onModalClosePress = useCallback(() => { + const handleModalClose = useCallback(() => { dispatch( clearPendingChanges({ section: 'settings.releaseProfiles', @@ -29,10 +29,10 @@ function EditReleaseProfileModal({ }, [dispatch, onModalClose]); return ( - + ); diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx index 9f672ea4a5..5b2b4289cc 100644 --- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx +++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.tsx @@ -63,11 +63,11 @@ interface EditReleaseProfileModalContentProps { onDeleteReleaseProfilePress?: () => void; } -function EditReleaseProfileModalContent( - props: EditReleaseProfileModalContentProps -) { - const { id, onModalClose, onDeleteReleaseProfilePress } = props; - +function EditReleaseProfileModalContent({ + id, + onModalClose, + onDeleteReleaseProfilePress, +}: EditReleaseProfileModalContentProps) { const { item, isFetching, isSaving, error, saveError, ...otherProps } = useSelector(createReleaseProfileSelector(id)); @@ -78,14 +78,9 @@ function EditReleaseProfileModalContent( useEffect(() => { if (!id) { - Object.keys(newReleaseProfile).forEach((name) => { - dispatch( - // @ts-expect-error 'setReleaseProfileValue' isn't typed yet - setReleaseProfileValue({ - name, - value: newReleaseProfile[name as keyof typeof newReleaseProfile], - }) - ); + Object.entries(newReleaseProfile).forEach(([name, value]) => { + // @ts-expect-error 'setReleaseProfileValue' isn't typed yet + dispatch(setReleaseProfileValue({ name, value })); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -95,7 +90,7 @@ function EditReleaseProfileModalContent( if (previousIsSaving && !isSaving && !saveError) { onModalClose(); } - }); + }, [previousIsSaving, isSaving, saveError, onModalClose]); const handleSavePress = useCallback(() => { dispatch(saveReleaseProfile({ id })); From 75c7a3cfc6932762f7e62d622ed8562076166f51 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 4 Oct 2024 20:07:22 -0700 Subject: [PATCH 12/27] Fixed: Ignore free space check before grabbing if directory is missing --- .../FreeSpaceSpecificationFixture.cs | 12 ++++++++++++ .../Specifications/FreeSpaceSpecification.cs | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs index 42f57fb6c8..04e489e034 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/FreeSpaceSpecificationFixture.cs @@ -1,3 +1,4 @@ +using System.IO; using FluentAssertions; using Moq; using NUnit.Framework; @@ -90,5 +91,16 @@ public void should_return_true_if_skip_free_space_check_is_true() Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); } + + [Test] + public void should_return_true_if_root_folder_is_not_available() + { + WithMinimumFreeSpace(150); + WithSize(100); + + Mocker.GetMock().Setup(s => s.GetAvailableSpace(It.IsAny())).Throws(); + + Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs index 1967bb1db6..e35a609462 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs @@ -1,3 +1,4 @@ +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; @@ -32,11 +33,21 @@ public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCrit } var size = subject.Release.Size; - var freeSpace = _diskProvider.GetAvailableSpace(subject.Movie.Path); + var path = subject.Movie.Path; + long? freeSpace = null; + + try + { + freeSpace = _diskProvider.GetAvailableSpace(path); + } + catch (DirectoryNotFoundException) + { + // Ignore so it'll be skipped in the following checks + } if (!freeSpace.HasValue) { - _logger.Debug("Unable to get available space for {0}. Skipping", subject.Movie.Path); + _logger.Debug("Unable to get available space for {0}. Skipping", path); return Decision.Accept(); } From 0deae95782f2cb21cbbbd15470d0fc8b037a7e61 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 6 Oct 2024 12:03:04 +0300 Subject: [PATCH 13/27] Bump version to 5.12.2 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f08d029828..169617dcff 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ variables: testsFolder: './_tests' yarnCacheFolder: $(Pipeline.Workspace)/.yarn nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages - majorVersion: '5.12.1' + majorVersion: '5.12.2' minorVersion: $[counter('minorVersion', 2000)] radarrVersion: '$(majorVersion).$(minorVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)' From da1b53b7e250df25ebfc5e02f32054d4c6cdced8 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 6 Oct 2024 12:38:46 +0300 Subject: [PATCH 14/27] Bump macOS runner version to 13 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 169617dcff..60bc575251 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,7 +20,7 @@ variables: innoVersion: '6.2.2' windowsImage: 'windows-2022' linuxImage: 'ubuntu-20.04' - macImage: 'macOS-12' + macImage: 'macOS-13' trigger: branches: From f6542bab0a6c05760b4d926d9d87720ed136f38a Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 4 Oct 2024 19:19:12 -0700 Subject: [PATCH 15/27] New: Use 307 redirect for requests missing URL Base --- src/Radarr.Http/Middleware/UrlBaseMiddleware.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Radarr.Http/Middleware/UrlBaseMiddleware.cs b/src/Radarr.Http/Middleware/UrlBaseMiddleware.cs index 091f2aaeb8..3f593851f2 100644 --- a/src/Radarr.Http/Middleware/UrlBaseMiddleware.cs +++ b/src/Radarr.Http/Middleware/UrlBaseMiddleware.cs @@ -20,6 +20,8 @@ public async Task InvokeAsync(HttpContext context) if (_urlBase.IsNotNullOrWhiteSpace() && context.Request.PathBase.Value.IsNullOrWhiteSpace()) { context.Response.Redirect($"{_urlBase}{context.Request.Path}{context.Request.QueryString}"); + context.Response.StatusCode = 307; + return; } From b29dee63f4b493c1e5e28a240bb05155bd7ff199 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 4 Oct 2024 19:50:49 +0300 Subject: [PATCH 16/27] Use the first allowed quality for cutoff met rejection message with disabled upgrades --- .../Specifications/UpgradeDiskSpecification.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index ae7ea369f4..f44249d341 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -37,6 +37,7 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se } file.Movie = subject.Movie; + var customFormats = _formatService.ParseCustomFormat(file); _logger.Debug("Comparing file quality with report. Existing file is {0} [{1}].", file.Quality, customFormats.ConcatToString()); @@ -48,8 +49,8 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se { _logger.Debug("Cutoff already met, rejecting."); - var qualityCutoffIndex = qualityProfile.GetIndex(qualityProfile.Cutoff); - var qualityCutoff = qualityProfile.Items[qualityCutoffIndex.Index]; + var cutoff = qualityProfile.UpgradeAllowed ? qualityProfile.Cutoff : qualityProfile.FirststAllowedQuality().Id; + var qualityCutoff = qualityProfile.Items[qualityProfile.GetIndex(cutoff).Index]; return Decision.Reject("Existing file meets cutoff: {0} [{1}]", qualityCutoff, customFormats.ConcatToString()); } From 7c243cb6e8ec9df123f6ecd4f5707668287b69a9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 8 Oct 2024 01:27:22 +0300 Subject: [PATCH 17/27] Fixed: Error updating providers with ID missing from JSON (cherry picked from commit c435fcd685cc97e98d14f747227eefd39e4d1164) --- src/Radarr.Api.V3/ProviderControllerBase.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Radarr.Api.V3/ProviderControllerBase.cs b/src/Radarr.Api.V3/ProviderControllerBase.cs index 12d506cdcc..3cfd1fb57b 100644 --- a/src/Radarr.Api.V3/ProviderControllerBase.cs +++ b/src/Radarr.Api.V3/ProviderControllerBase.cs @@ -86,9 +86,16 @@ public ActionResult CreateProvider([FromBody] TProviderResour [RestPutById] [Consumes("application/json")] [Produces("application/json")] - public ActionResult UpdateProvider([FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) + public ActionResult UpdateProvider([FromRoute] int id, [FromBody] TProviderResource providerResource, [FromQuery] bool forceSave = false) { - var existingDefinition = _providerFactory.Find(providerResource.Id); + // TODO: Remove fallback to Id from body in next API version bump + var existingDefinition = _providerFactory.Find(id) ?? _providerFactory.Find(providerResource.Id); + + if (existingDefinition == null) + { + return NotFound(); + } + var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceSave, false); // Compare settings separately because they are not serialized with the definition. @@ -105,7 +112,7 @@ public ActionResult UpdateProvider([FromBody] TProviderResour _providerFactory.Update(providerDefinition); } - return Accepted(providerResource.Id); + return Accepted(existingDefinition.Id); } [HttpPut("bulk")] From 38bd06096047dfc235451c63938a03c929190df1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 4 Oct 2024 12:59:41 +0300 Subject: [PATCH 18/27] Convert FormInputButton to TypeScript --- .../src/Components/Form/FormInputButton.js | 54 ------------------- .../src/Components/Form/FormInputButton.tsx | 38 +++++++++++++ 2 files changed, 38 insertions(+), 54 deletions(-) delete mode 100644 frontend/src/Components/Form/FormInputButton.js create mode 100644 frontend/src/Components/Form/FormInputButton.tsx diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js deleted file mode 100644 index a7145363af..0000000000 --- a/frontend/src/Components/Form/FormInputButton.js +++ /dev/null @@ -1,54 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import { kinds } from 'Helpers/Props'; -import styles from './FormInputButton.css'; - -function FormInputButton(props) { - const { - className, - canSpin, - isLastButton, - ...otherProps - } = props; - - if (canSpin) { - return ( - - ); - } - - return ( - -
-
- - ); - } -} - -NamingModal.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isOpen: PropTypes.bool.isRequired, - advancedSettings: PropTypes.bool.isRequired, - additional: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -NamingModal.defaultProps = { - additional: false -}; - -export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx new file mode 100644 index 0000000000..bc189d5210 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx @@ -0,0 +1,457 @@ +import React, { useCallback, useState } from 'react'; +import FieldSet from 'Components/FieldSet'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import { icons, sizes } from 'Helpers/Props'; +import NamingConfig from 'typings/Settings/NamingConfig'; +import translate from 'Utilities/String/translate'; +import NamingOption from './NamingOption'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingModal.css'; + +const separatorOptions: { key: TokenSeparator; value: string }[] = [ + { + key: ' ', + get value() { + return `${translate('Space')} ( )`; + }, + }, + { + key: '.', + get value() { + return `${translate('Period')} (.)`; + }, + }, + { + key: '_', + get value() { + return `${translate('Underscore')} (_)`; + }, + }, + { + key: '-', + get value() { + return `${translate('Dash')} (-)`; + }, + }, +]; + +const caseOptions: { key: TokenCase; value: string }[] = [ + { + key: 'title', + get value() { + return translate('DefaultCase'); + }, + }, + { + key: 'lower', + get value() { + return translate('Lowercase'); + }, + }, + { + key: 'upper', + get value() { + return translate('Uppercase'); + }, + }, +]; + +const fileNameTokens = [ + { + token: '{Movie Title} - {Quality Full}', + example: 'Movie Title (2010) - HDTV-720p Proper', + }, +]; + +const movieTokens = [ + { token: '{Movie Title}', example: "Movie's Title", footNote: true }, + { token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true }, + { token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true }, + { + token: '{Movie CleanTitle:DE}', + example: 'Titel des Films', + footNote: true, + }, + { token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true }, + { + token: '{Movie CleanTitleThe}', + example: 'Movies Title, The', + footNote: true, + }, + { token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true }, + { + token: '{Movie CleanOriginalTitle}', + example: 'Τίτλος ταινίας', + footNote: true, + }, + { token: '{Movie TitleFirstCharacter}', example: 'M' }, + { token: '{Movie TitleFirstCharacter:DE}', example: 'T' }, + { + token: '{Movie Collection}', + example: 'The Movie Collection', + footNote: true, + }, + { token: '{Movie Certification}', example: 'R' }, + { token: '{Release Year}', example: '2009' }, +]; + +const movieIdTokens = [ + { token: '{ImdbId}', example: 'tt12345' }, + { token: '{TmdbId}', example: '123456' }, +]; + +const qualityTokens = [ + { token: '{Quality Full}', example: 'HDTV-720p Proper' }, + { token: '{Quality Title}', example: 'HDTV-720p' }, +]; + +const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true }, + + { token: '{MediaInfo AudioCodec}', example: 'DTS' }, + { token: '{MediaInfo AudioChannels}', example: '5.1' }, + { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true }, + { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true }, + + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo VideoBitDepth}', example: '10' }, + { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, + { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' }, + { token: '{MediaInfo 3D}', example: '3D' }, +]; + +const releaseGroupTokens = [ + { token: '{Release Group}', example: 'Rls Grp', footNote: true }, +]; + +const editionTokens = [ + { token: '{Edition Tags}', example: 'IMAX', footNote: true }, +]; + +const customFormatTokens = [ + { token: '{Custom Formats}', example: 'Surround Sound x264' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' }, +]; + +const originalTokens = [ + { token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' }, + { token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' }, +]; + +interface NamingModalProps { + isOpen: boolean; + name: keyof Pick; + value: string; + advancedSettings: boolean; + movie?: boolean; + additional?: boolean; + onInputChange: ({ name, value }: { name: string; value: string }) => void; + onModalClose: () => void; +} + +function NamingModal(props: NamingModalProps) { + const { + isOpen, + name, + value, + advancedSettings, + movie = false, + additional = false, + onInputChange, + onModalClose, + } = props; + + const [tokenSeparator, setTokenSeparator] = useState(' '); + const [tokenCase, setTokenCase] = useState('title'); + const [selectionStart, setSelectionStart] = useState(null); + const [selectionEnd, setSelectionEnd] = useState(null); + + const handleTokenSeparatorChange = useCallback( + ({ value }: { value: TokenSeparator }) => { + setTokenSeparator(value); + }, + [setTokenSeparator] + ); + + const handleTokenCaseChange = useCallback( + ({ value }: { value: TokenCase }) => { + setTokenCase(value); + }, + [setTokenCase] + ); + + const handleInputSelectionChange = useCallback( + (selectionStart: number, selectionEnd: number) => { + setSelectionStart(selectionStart); + setSelectionEnd(selectionEnd); + }, + [setSelectionStart, setSelectionEnd] + ); + + const handleOptionPress = useCallback( + ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => { + if (isFullFilename) { + onInputChange({ name, value: tokenValue }); + } else if (selectionStart == null || selectionEnd == null) { + onInputChange({ + name, + value: `${value}${tokenValue}`, + }); + } else { + const start = value.substring(0, selectionStart); + const end = value.substring(selectionEnd); + const newValue = `${start}${tokenValue}${end}`; + + onInputChange({ name, value: newValue }); + + setSelectionStart(newValue.length - 1); + setSelectionEnd(newValue.length - 1); + } + }, + [name, value, selectionEnd, selectionStart, onInputChange] + ); + + return ( + + + + {movie ? translate('FileNameTokens') : translate('FolderNameTokens')} + + + +
+ + + +
+ + {advancedSettings ? null : ( +
+
+ {fileNameTokens.map(({ token, example }) => ( + + ))} +
+
+ )} + +
+
+ {movieTokens.map(({ token, example, footNote }) => { + return ( + + ); + })} +
+ +
+ + +
+
+ +
+
+ {movieIdTokens.map(({ token, example }) => { + return ( + + ); + })} +
+
+ + {additional ? ( +
+
+
+ {qualityTokens.map(({ token, example }) => { + return ( + + ); + })} +
+
+ +
+
+ {mediaInfoTokens.map(({ token, example, footNote }) => { + return ( + + ); + })} +
+ +
+ + +
+
+ +
+
+ {releaseGroupTokens.map(({ token, example, footNote }) => { + return ( + + ); + })} +
+ +
+ + +
+
+ +
+
+ {editionTokens.map(({ token, example, footNote }) => { + return ( + + ); + })} +
+ +
+ + +
+
+ +
+
+ {customFormatTokens.map(({ token, example }) => { + return ( + + ); + })} +
+
+ +
+
+ {originalTokens.map(({ token, example }) => { + return ( + + ); + })} +
+
+
+ ) : null} +
+ + + + + + +
+
+ ); +} + +export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index a891a5ddd4..c744989971 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -46,6 +46,10 @@ } } +.title { + text-transform: none; +} + .lower { text-transform: lowercase; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts index a060f62182..5c50bfab2b 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'lower': string; 'option': string; 'small': string; + 'title': string; 'token': string; 'upper': string; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js deleted file mode 100644 index 6373c11e39..0000000000 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js +++ /dev/null @@ -1,93 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons, sizes } from 'Helpers/Props'; -import styles from './NamingOption.css'; - -class NamingOption extends Component { - - // - // Listeners - - onPress = () => { - const { - token, - tokenSeparator, - tokenCase, - isFullFilename, - onPress - } = this.props; - - let tokenValue = token; - - tokenValue = tokenValue.replace(/ /g, tokenSeparator); - - if (tokenCase === 'lower') { - tokenValue = token.toLowerCase(); - } else if (tokenCase === 'upper') { - tokenValue = token.toUpperCase(); - } - - onPress({ isFullFilename, tokenValue }); - }; - - // - // Render - render() { - const { - token, - tokenSeparator, - example, - footNote, - tokenCase, - isFullFilename, - size - } = this.props; - - return ( - -
- {token.replace(/ /g, tokenSeparator)} -
- -
- {example.replace(/ /g, tokenSeparator)} - - { - footNote !== 0 && - - } -
- - ); - } -} - -NamingOption.propTypes = { - token: PropTypes.string.isRequired, - example: PropTypes.string.isRequired, - footNote: PropTypes.number.isRequired, - tokenSeparator: PropTypes.string.isRequired, - tokenCase: PropTypes.string.isRequired, - isFullFilename: PropTypes.bool.isRequired, - size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]), - onPress: PropTypes.func.isRequired -}; - -NamingOption.defaultProps = { - footNote: 0, - size: sizes.SMALL, - isFullFilename: false -}; - -export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx new file mode 100644 index 0000000000..e9bcf11ff4 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import React, { useCallback } from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import { Size } from 'Helpers/Props/sizes'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingOption.css'; + +interface NamingOptionProps { + token: string; + tokenSeparator: TokenSeparator; + example: string; + tokenCase: TokenCase; + isFullFilename?: boolean; + footNote?: boolean; + size?: Extract; + onPress: ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => void; +} + +function NamingOption(props: NamingOptionProps) { + const { + token, + tokenSeparator, + example, + tokenCase, + isFullFilename = false, + footNote = false, + size = 'small', + onPress, + } = props; + + const handlePress = useCallback(() => { + let tokenValue = token; + + tokenValue = tokenValue.replace(/ /g, tokenSeparator); + + if (tokenCase === 'lower') { + tokenValue = token.toLowerCase(); + } else if (tokenCase === 'upper') { + tokenValue = token.toUpperCase(); + } + + onPress({ isFullFilename, tokenValue }); + }, [token, tokenCase, tokenSeparator, isFullFilename, onPress]); + + return ( + +
{token.replace(/ /g, tokenSeparator)}
+ +
+ {example.replace(/ /g, tokenSeparator)} + + {footNote ? ( + + ) : null} +
+ + ); +} + +export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts new file mode 100644 index 0000000000..280ef307d5 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts @@ -0,0 +1,3 @@ +type TokenCase = 'title' | 'lower' | 'upper'; + +export default TokenCase; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts new file mode 100644 index 0000000000..5ef86a6a14 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts @@ -0,0 +1,3 @@ +type TokenSeparator = ' ' | '.' | '_' | '-'; + +export default TokenSeparator; diff --git a/frontend/src/typings/Settings/NamingConfig.ts b/frontend/src/typings/Settings/NamingConfig.ts new file mode 100644 index 0000000000..054208395f --- /dev/null +++ b/frontend/src/typings/Settings/NamingConfig.ts @@ -0,0 +1,14 @@ +type ColonReplacementFormat = + | 'delete' + | 'dash' + | 'spaceDash' + | 'spaceDashSpace' + | 'smart'; + +export default interface NamingConfig { + renameMovies: boolean; + replaceIllegalCharacters: boolean; + colonReplacementFormat: ColonReplacementFormat; + standardMovieFormat: string; + movieFolderFormat: string; +} diff --git a/frontend/src/typings/Settings/NamingExample.ts b/frontend/src/typings/Settings/NamingExample.ts new file mode 100644 index 0000000000..8f738362c6 --- /dev/null +++ b/frontend/src/typings/Settings/NamingExample.ts @@ -0,0 +1,4 @@ +export default interface NamingExample { + movieExample: string; + movieFolderExample: string; +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index a3beafbbd7..ae7a54cc5a 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -641,6 +641,7 @@ "FocusSearchBox": "Focus Search Box", "Folder": "Folder", "FolderMoveRenameWarning": "This will also rename the movie folder per the movie folder format in settings.", + "FolderNameTokens": "Folder Name Tokens", "Folders": "Folders", "FollowPerson": "Follow Person", "Forecast": "Forecast", From ce4477eeacc05356752e7757ae35274e68a590ca Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 4 Oct 2024 19:22:39 +0300 Subject: [PATCH 20/27] Improve filename examples for movies naming --- .../MediaManagement/Naming/Naming.tsx | 1 - .../MediaManagement/Naming/NamingModal.tsx | 24 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.tsx b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx index 329eddbf03..0a09fc4d0f 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.tsx +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx @@ -258,7 +258,6 @@ function Naming() { {namingModalOptions ? ( ; value: string; - advancedSettings: boolean; movie?: boolean; additional?: boolean; onInputChange: ({ name, value }: { name: string; value: string }) => void; @@ -165,7 +178,6 @@ function NamingModal(props: NamingModalProps) { isOpen, name, value, - advancedSettings, movie = false, additional = false, onInputChange, @@ -254,7 +266,7 @@ function NamingModal(props: NamingModalProps) { />
- {advancedSettings ? null : ( + {movie ? (
{fileNameTokens.map(({ token, example }) => ( @@ -271,7 +283,7 @@ function NamingModal(props: NamingModalProps) { ))}
- )} + ) : null}
From 7f3d107eda88eaa3c5b7e131bdb0f4d6714b79d4 Mon Sep 17 00:00:00 2001 From: Treycos <19551067+Treycos@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:09:55 +0200 Subject: [PATCH 21/27] Convert ClipboardButton to TypeScript (cherry picked from commit 99fc52039f44264c83d939e5f096d8e16d2f3355) Closes #10452 --- .../src/Components/Link/ClipboardButton.js | 139 ------------------ .../src/Components/Link/ClipboardButton.tsx | 69 +++++++++ frontend/src/Utilities/getUniqueElementId.js | 6 +- package.json | 1 - yarn.lock | 31 ---- 5 files changed, 73 insertions(+), 173 deletions(-) delete mode 100644 frontend/src/Components/Link/ClipboardButton.js create mode 100644 frontend/src/Components/Link/ClipboardButton.tsx diff --git a/frontend/src/Components/Link/ClipboardButton.js b/frontend/src/Components/Link/ClipboardButton.js deleted file mode 100644 index 55843f05ff..0000000000 --- a/frontend/src/Components/Link/ClipboardButton.js +++ /dev/null @@ -1,139 +0,0 @@ -import Clipboard from 'clipboard'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FormInputButton from 'Components/Form/FormInputButton'; -import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; -import getUniqueElememtId from 'Utilities/getUniqueElementId'; -import styles from './ClipboardButton.css'; - -class ClipboardButton extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._id = getUniqueElememtId(); - this._successTimeout = null; - this._testResultTimeout = null; - - this.state = { - showSuccess: false, - showError: false - }; - } - - componentDidMount() { - this._clipboard = new Clipboard(`#${this._id}`, { - text: () => this.props.value, - container: document.getElementById(this._id) - }); - - this._clipboard.on('success', this.onSuccess); - } - - componentDidUpdate() { - const { - showSuccess, - showError - } = this.state; - - if (showSuccess || showError) { - this._testResultTimeout = setTimeout(this.resetState, 3000); - } - } - - componentWillUnmount() { - if (this._clipboard) { - this._clipboard.destroy(); - } - - if (this._testResultTimeout) { - clearTimeout(this._testResultTimeout); - } - } - - // - // Control - - resetState = () => { - this.setState({ - showSuccess: false, - showError: false - }); - }; - - // - // Listeners - - onSuccess = () => { - this.setState({ - showSuccess: true - }); - }; - - onError = () => { - this.setState({ - showError: true - }); - }; - - // - // Render - - render() { - const { - value, - className, - ...otherProps - } = this.props; - - const { - showSuccess, - showError - } = this.state; - - const showStateIcon = showSuccess || showError; - const iconName = showError ? icons.DANGER : icons.CHECK; - const iconKind = showError ? kinds.DANGER : kinds.SUCCESS; - - return ( - - - { - showSuccess && - - - - } - - { - - - - } - - - ); - } -} - -ClipboardButton.propTypes = { - className: PropTypes.string.isRequired, - value: PropTypes.string.isRequired -}; - -ClipboardButton.defaultProps = { - className: styles.button -}; - -export default ClipboardButton; diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx new file mode 100644 index 0000000000..09095ae743 --- /dev/null +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import FormInputButton from 'Components/Form/FormInputButton'; +import Icon from 'Components/Icon'; +import { icons, kinds } from 'Helpers/Props'; +import { ButtonProps } from './Button'; +import styles from './ClipboardButton.css'; + +export interface ClipboardButtonProps extends Omit { + value: string; +} + +export type ClipboardState = 'success' | 'error' | null; + +export default function ClipboardButton({ + id, + value, + className = styles.button, + ...otherProps +}: ClipboardButtonProps) { + const [state, setState] = useState(null); + + useEffect(() => { + if (!state) { + return; + } + + const timeoutId = setTimeout(() => { + setState(null); + }, 3000); + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [state]); + + const handleClick = useCallback(async () => { + try { + await navigator.clipboard.writeText(value); + setState('success'); + } catch (_) { + setState('error'); + } + }, [value]); + + return ( + + + {state ? ( + + + + ) : null} + + + + + + + ); +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js index dae5150b7f..1b380851dd 100644 --- a/frontend/src/Utilities/getUniqueElementId.js +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -1,7 +1,9 @@ let i = 0; -// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) - +/** + * @deprecated Use React's useId() instead + * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + */ export default function getUniqueElementId() { return `id-${i++}`; } diff --git a/package.json b/package.json index 3663a44dcf..b3c4378c66 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "classnames": "2.3.2", - "clipboard": "2.0.11", "connected-react-router": "6.9.3", "element-class": "0.2.2", "filesize": "10.0.7", diff --git a/yarn.lock b/yarn.lock index c2cda529da..2f8aa32331 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2461,15 +2461,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2869,11 +2860,6 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -3813,13 +3799,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6269,11 +6248,6 @@ section-iterator@^2.0.0: resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" integrity sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ== -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -6785,11 +6759,6 @@ time-stamp@^1.0.0: resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" integrity sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" From f0f828491b7002cfa0613241b2b4e73c32d6c4e9 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 27 Sep 2024 10:26:47 +0300 Subject: [PATCH 22/27] Fixed: Copy to clipboard in non-secure contexts (cherry picked from commit 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3) Closes #10525 --- .../src/Components/Link/ClipboardButton.tsx | 11 ++++++++-- package.json | 1 + yarn.lock | 21 ++++++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx index 09095ae743..dfce115ac9 100644 --- a/frontend/src/Components/Link/ClipboardButton.tsx +++ b/frontend/src/Components/Link/ClipboardButton.tsx @@ -1,3 +1,4 @@ +import copy from 'copy-to-clipboard'; import React, { useCallback, useEffect, useState } from 'react'; import FormInputButton from 'Components/Form/FormInputButton'; import Icon from 'Components/Icon'; @@ -37,10 +38,16 @@ export default function ClipboardButton({ const handleClick = useCallback(async () => { try { - await navigator.clipboard.writeText(value); + if ('clipboard' in navigator) { + await navigator.clipboard.writeText(value); + } else { + copy(value); + } + setState('success'); - } catch (_) { + } catch (e) { setState('error'); + console.error(`Failed to copy to clipboard`, e); } }, [value]); diff --git a/package.json b/package.json index b3c4378c66..17a5fe5490 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/react-dom": "18.2.25", "classnames": "2.3.2", "connected-react-router": "6.9.3", + "copy-to-clipboard": "3.3.3", "element-class": "0.2.2", "filesize": "10.0.7", "fuse.js": "6.6.2", diff --git a/yarn.lock b/yarn.lock index 2f8aa32331..7f50c6454f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2598,7 +2598,21 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -core-js-compat@^3.37.1, core-js-compat@^3.38.0: +copy-to-clipboard@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + +core-js-compat@^3.37.1: + version "3.38.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.0.tgz#d93393b1aa346b6ee683377b0c31172ccfe607aa" + integrity sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A== + dependencies: + browserslist "^4.23.3" + +core-js-compat@^3.38.0: version "3.38.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== @@ -6812,6 +6826,11 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + "tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" From 9a22e1c791750fb356a98cba3e402cb9cc348a36 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Tue, 8 Oct 2024 02:15:07 +0300 Subject: [PATCH 23/27] Bump browserslist-db --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7f50c6454f..735aac2e38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2392,9 +2392,9 @@ camelcase@^5.3.1: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-lite@^1.0.30001646: - version "1.0.30001651" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138" - integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== + version "1.0.30001667" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz" + integrity sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== chalk@^1.1.3: version "1.1.3" From 8b7884deb0a6ed5d45e01462600a90706f9f3208 Mon Sep 17 00:00:00 2001 From: Servarr Date: Mon, 7 Oct 2024 23:35:10 +0000 Subject: [PATCH 24/27] Automated API Docs update --- src/Radarr.Api.V3/openapi.json | 85 ++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/Radarr.Api.V3/openapi.json b/src/Radarr.Api.V3/openapi.json index 8c77ea71fd..30dc950c9b 100644 --- a/src/Radarr.Api.V3/openapi.json +++ b/src/Radarr.Api.V3/openapi.json @@ -1907,6 +1907,15 @@ "DownloadClient" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -1914,14 +1923,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -2836,6 +2837,15 @@ "ImportList" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -2843,14 +2853,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -3611,6 +3613,15 @@ "Indexer" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -3618,14 +3629,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -4520,6 +4523,15 @@ "Metadata" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -4527,14 +4539,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -5704,6 +5708,15 @@ "Notification" ], "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, { "name": "forceSave", "in": "query", @@ -5711,14 +5724,6 @@ "type": "boolean", "default": false } - }, - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { From 958a863d8f62bd24714ab132f85c48f10251dcdb Mon Sep 17 00:00:00 2001 From: Jared Ledvina Date: Mon, 7 Oct 2024 18:25:52 -0400 Subject: [PATCH 25/27] Recompare file size after import file if necessary (cherry picked from commit 6660db22ecf53d7747e3abc400529669ea779fa1) --- .../Disk/DiskTransferService.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index fb7d93f48c..2da930a787 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -474,12 +474,7 @@ private void TryCopyFileVerified(string sourcePath, string targetPath, long orig try { _diskProvider.CopyFile(sourcePath, targetPath); - - var targetSize = _diskProvider.GetFileSize(targetPath); - if (targetSize != originalSize) - { - throw new IOException(string.Format("File copy incomplete. [{0}] was {1} bytes long instead of {2} bytes.", targetPath, targetSize, originalSize)); - } + VerifyFile(sourcePath, targetPath, originalSize, "copy"); } catch { @@ -493,12 +488,7 @@ private void TryMoveFileVerified(string sourcePath, string targetPath, long orig try { _diskProvider.MoveFile(sourcePath, targetPath); - - var targetSize = _diskProvider.GetFileSize(targetPath); - if (targetSize != originalSize) - { - throw new IOException(string.Format("File move incomplete, data loss may have occurred. [{0}] was {1} bytes long instead of the expected {2}.", targetPath, targetSize, originalSize)); - } + VerifyFile(sourcePath, targetPath, originalSize, "move"); } catch (Exception ex) { @@ -511,6 +501,27 @@ private void TryMoveFileVerified(string sourcePath, string targetPath, long orig } } + private void VerifyFile(string sourcePath, string targetPath, long originalSize, string action) + { + var targetSize = _diskProvider.GetFileSize(targetPath); + + if (targetSize == originalSize) + { + return; + } + + _logger.Debug("File {0} incomplete, waiting in case filesystem is not synchronized. [{1}] was {2} bytes long instead of the expected {3}.", action, targetPath, targetSize, originalSize); + WaitForIO(); + targetSize = _diskProvider.GetFileSize(targetPath); + + if (targetSize == originalSize) + { + return; + } + + throw new IOException(string.Format("File {0} incomplete, data loss may have occurred. [{1}] was {2} bytes long instead of the expected {3}.", action, targetPath, targetSize, originalSize)); + } + private bool ShouldIgnore(DirectoryInfo folder) { if (folder.Name.StartsWith(".nfs")) From 2a3d595a66244c3256b87798c3f385bf411e7cc1 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 8 Oct 2024 10:25:23 +0000 Subject: [PATCH 26/27] Multiple Translations updated by Weblate ignore-downstream Co-authored-by: Anonymous Co-authored-by: Ardenet <1213193613@qq.com> Co-authored-by: Weblate Co-authored-by: angelsky11 Co-authored-by: anne Co-authored-by: fordas Co-authored-by: jsain Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/ Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/ Translation: Servarr/Radarr --- src/NzbDrone.Core/Localization/Core/ar.json | 3 +- src/NzbDrone.Core/Localization/Core/bg.json | 3 +- src/NzbDrone.Core/Localization/Core/ca.json | 3 +- src/NzbDrone.Core/Localization/Core/cs.json | 3 +- src/NzbDrone.Core/Localization/Core/da.json | 3 +- src/NzbDrone.Core/Localization/Core/de.json | 3 +- src/NzbDrone.Core/Localization/Core/el.json | 3 +- src/NzbDrone.Core/Localization/Core/es.json | 7 +- src/NzbDrone.Core/Localization/Core/fi.json | 3 +- src/NzbDrone.Core/Localization/Core/fr.json | 8 +- src/NzbDrone.Core/Localization/Core/he.json | 3 +- src/NzbDrone.Core/Localization/Core/hi.json | 3 +- src/NzbDrone.Core/Localization/Core/hr.json | 48 +++++++- src/NzbDrone.Core/Localization/Core/hu.json | 3 +- src/NzbDrone.Core/Localization/Core/is.json | 3 +- src/NzbDrone.Core/Localization/Core/it.json | 3 +- src/NzbDrone.Core/Localization/Core/ja.json | 3 +- src/NzbDrone.Core/Localization/Core/nl.json | 3 +- src/NzbDrone.Core/Localization/Core/pt.json | 3 +- .../Localization/Core/pt_BR.json | 3 +- src/NzbDrone.Core/Localization/Core/ro.json | 3 +- src/NzbDrone.Core/Localization/Core/ru.json | 3 +- src/NzbDrone.Core/Localization/Core/th.json | 3 +- src/NzbDrone.Core/Localization/Core/tr.json | 3 +- src/NzbDrone.Core/Localization/Core/uk.json | 3 +- src/NzbDrone.Core/Localization/Core/vi.json | 3 +- .../Localization/Core/zh_CN.json | 110 ++++++++++-------- 27 files changed, 160 insertions(+), 82 deletions(-) diff --git a/src/NzbDrone.Core/Localization/Core/ar.json b/src/NzbDrone.Core/Localization/Core/ar.json index e210b6cd53..b437f7316b 100644 --- a/src/NzbDrone.Core/Localization/Core/ar.json +++ b/src/NzbDrone.Core/Localization/Core/ar.json @@ -1073,5 +1073,6 @@ "ShowDigitalRelease": "عرض تاريخ الإصدار السينمائي", "ShowDigitalReleaseHelpText": "عرض تاريخ الإصدار تحت الملصق", "ShowPhysicalRelease": "تاريخ الإصدار المادي", - "ShowPhysicalReleaseHelpText": "عرض تاريخ الإصدار تحت الملصق" + "ShowPhysicalReleaseHelpText": "عرض تاريخ الإصدار تحت الملصق", + "FolderNameTokens": "رموز اسم الملف" } diff --git a/src/NzbDrone.Core/Localization/Core/bg.json b/src/NzbDrone.Core/Localization/Core/bg.json index f3c0bb1792..b80b1e4019 100644 --- a/src/NzbDrone.Core/Localization/Core/bg.json +++ b/src/NzbDrone.Core/Localization/Core/bg.json @@ -1070,5 +1070,6 @@ "ShowPhysicalReleaseHelpText": "Показване на датата на пускане под постер", "ShowDigitalRelease": "Показване на датата на излизане на киното", "ShowDigitalReleaseHelpText": "Показване на датата на пускане под постер", - "ShowPhysicalRelease": "Дата на физическото издаване" + "ShowPhysicalRelease": "Дата на физическото издаване", + "FolderNameTokens": "Токени за име на файл" } diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 51a96b3cfc..d84131e642 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -1385,5 +1385,6 @@ "ShowPhysicalReleaseHelpText": "Mostra la data de llançament sota el cartell", "ShowDigitalRelease": "Mostra la data d'estrena", "ShowDigitalReleaseHelpText": "Mostra la data de llançament sota el cartell", - "Logout": "Tanca la sessió" + "Logout": "Tanca la sessió", + "FolderNameTokens": "Testimonis de nom de fitxer" } diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 935b23015f..c332a3f5db 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -1200,5 +1200,6 @@ "ShowPhysicalRelease": "Datum fyzického vydání", "ShowPhysicalReleaseHelpText": "Zobrazit datum vydání pod plakátem", "ShowDigitalRelease": "Zobrazit datum vydání kina", - "MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "Několik kořenových adresářů chybí pro seznamy importu: {rootFoldersInfo}" + "MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "Několik kořenových adresářů chybí pro seznamy importu: {rootFoldersInfo}", + "FolderNameTokens": "Tokeny názvů souborů" } diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 43101c34d6..e96e44af24 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -1092,5 +1092,6 @@ "ShowPhysicalReleaseHelpText": "Vis udgivelsesdato under plakat", "ShowDigitalRelease": "Vis biografens udgivelsesdato", "ShowDigitalReleaseHelpText": "Vis udgivelsesdato under plakat", - "ShowPhysicalRelease": "Fysisk udgivelsesdato" + "ShowPhysicalRelease": "Fysisk udgivelsesdato", + "FolderNameTokens": "Filnavn tokens" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index d29e5dcd67..bd21479cad 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -1466,5 +1466,6 @@ "ShowDigitalRelease": "Erscheinungsdatum des Kinos anzeigen", "ShowDigitalReleaseHelpText": "Kino-Erscheinungsdatum unter Poster anzeigen", "ShowPhysicalRelease": "Disc Veröffentlichungsdatum", - "ShowPhysicalReleaseHelpText": "Kino-Erscheinungsdatum unter Poster anzeigen" + "ShowPhysicalReleaseHelpText": "Kino-Erscheinungsdatum unter Poster anzeigen", + "FolderNameTokens": "Dateinamen Teile" } diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 050c80e08c..90886d17d2 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -1232,5 +1232,6 @@ "ShowPhysicalReleaseHelpText": "Εμφάνιση ημερομηνίας κυκλοφορίας στην αφίσα", "ShowDigitalRelease": "Εμφάνιση ημερομηνίας κυκλοφορίας κινηματογράφου", "SmartReplace": "Έξυπνη Αντικατάσταση", - "SmartReplaceHint": "Παύλα ή Κενό-Παύλα ανάλογα με το όνομα" + "SmartReplaceHint": "Παύλα ή Κενό-Παύλα ανάλογα με το όνομα", + "FolderNameTokens": "Διακριτικά ονόματος αρχείου" } diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index c8f6983938..28a509be23 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -1218,7 +1218,7 @@ "InteractiveImportNoMovie": "Debe elegirse una película para cada archivo seleccionado", "ManualGrab": "Captura manual", "MatchedToMovie": "Coincidencia con la película", - "HealthMessagesInfoBox": "Puede encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando sus [logs]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro servicio de asistencia en los enlaces que aparecen a continuación.", + "HealthMessagesInfoBox": "Puedes encontrar más información sobre la causa de estos mensajes de comprobación de salud haciendo clic en el enlace wiki (icono de libro) al final de la fila, o comprobando tus [registros]({link}). Si tienes dificultades para interpretar estos mensajes, puedes ponerte en contacto con nuestro soporte en los enlaces que aparecen a continuación.", "ManageFiles": "Gestionar archivos", "MovieFileDeleted": "Archivo de película eliminado", "MovieFileRenamedTooltip": "Archivo de película renombrado", @@ -1650,7 +1650,7 @@ "DelayMinutes": "{delay} minutos", "DelayProfileProtocol": "Protocolo: {preferredProtocol}", "DeleteReleaseProfile": "Eliminar perfil de lanzamiento", - "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar este perfil de lanzamiento '{name}'?", + "DeleteReleaseProfileMessageText": "¿Estás seguro que quieres eliminar el perfil de lanzamiento '{name}'?", "DeleteSpecification": "Eliminar especificación", "DownloadClientDelugeValidationLabelPluginFailure": "La configuración de etiqueta falló", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} no pudo añadir la etiqueta a {clientName}.", @@ -1835,5 +1835,6 @@ "InCinemasMovieAvailabilityDescription": "Las películas se consideran disponibles tan pronto como llegan a los cines.", "ReleasedMovieAvailabilityDescription": "Las películas se consideran disponibles tan pronto como se lanzan la versión en Blu-Ray o en streaming.", "SmartReplaceHint": "Raya o barra espaciadora según el nombre", - "SmartReplace": "Reemplazo inteligente" + "SmartReplace": "Reemplazo inteligente", + "FolderNameTokens": "Tokens de nombre de carpeta" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index d7e4909ee8..41d5ac72f6 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -1759,5 +1759,6 @@ "ShowPhysicalReleaseHelpText": "Näytä teatterijulkaisun päiväys julisteen alla.", "Logout": "Kirjaudu ulos", "ShowTraktRatingPosterHelpText": "Näytä Tomato-arvio julisteen alla.", - "SmartReplaceHint": "Yhdysmerkki tai välilyönti nimen perusteella" + "SmartReplaceHint": "Yhdysmerkki tai välilyönti nimen perusteella", + "FolderNameTokens": "Tiedostonimen muuttujat" } diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index dc5e22cb4b..9da523bd0c 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -1,6 +1,6 @@ { "IndexerStatusCheckAllClientMessage": "Tous les indexeurs sont indisponibles en raison d'échecs", - "IndexerSearchCheckNoInteractiveMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée, {appName} ne fournira aucun résultat de recherche interactive", + "IndexerSearchCheckNoInteractiveMessage": "Aucun indexeur n'est disponible avec la recherche interactive activée, {appName} ne fournira aucun résultats de recherche interactive", "IndexerSearchCheckNoAvailableIndexersMessage": "Tous les indexeurs compatibles avec la recherche sont temporairement indisponibles en raison d'erreurs d'indexation récentes", "IndexerSearchCheckNoAutomaticMessage": "Aucun indexeur disponible avec la recherche automatique activée, {appName} ne fournira aucun résultat de recherche automatique", "Indexers": "Indexeurs", @@ -1820,5 +1820,9 @@ "NotificationsGotifySettingsMetadataLinks": "Liens de métadonnées", "ShowTraktRatingPosterHelpText": "Affiche la note Tomate sous l'affiche", "SmartReplace": "Remplacement intelligent", - "SmartReplaceHint": "Tiret ou espace puis tiret selon le nom" + "SmartReplaceHint": "Tiret ou espace puis tiret selon le nom", + "AnnouncedMovieAvailabilityDescription": "Les films sont considérés disponibles dès qu'ils sont ajouté à {appName}.", + "CustomFormatsSpecificationExceptLanguage": "Excepté Langue", + "CustomFormatsSpecificationExceptLanguageHelpText": "Corresponf si l'autre langue que celle sélectionné est présente", + "FolderNameTokens": "Jetons de nom de fichier" } diff --git a/src/NzbDrone.Core/Localization/Core/he.json b/src/NzbDrone.Core/Localization/Core/he.json index 78c6a88a69..774e1d9e17 100644 --- a/src/NzbDrone.Core/Localization/Core/he.json +++ b/src/NzbDrone.Core/Localization/Core/he.json @@ -1115,5 +1115,6 @@ "ShowDigitalReleaseHelpText": "הצג תאריך יציאה תחת הכרזה", "ShowPhysicalRelease": "תאריך שחרור פיזי", "ShowPhysicalReleaseHelpText": "הצג תאריך יציאה תחת הכרזה", - "MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "מספר תיקיות אב חסרות לייבוא רשימות: {rootFoldersInfo}" + "MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "מספר תיקיות אב חסרות לייבוא רשימות: {rootFoldersInfo}", + "FolderNameTokens": "אסימונים לשם קובץ" } diff --git a/src/NzbDrone.Core/Localization/Core/hi.json b/src/NzbDrone.Core/Localization/Core/hi.json index df8be2efc9..82e0358987 100644 --- a/src/NzbDrone.Core/Localization/Core/hi.json +++ b/src/NzbDrone.Core/Localization/Core/hi.json @@ -1068,5 +1068,6 @@ "ShowDigitalReleaseHelpText": "पोस्टर के तहत रिलीज की तारीख दिखाएं", "ShowPhysicalRelease": "शारीरिक रिलीज की तारीख", "ShowPhysicalReleaseHelpText": "पोस्टर के तहत रिलीज की तारीख दिखाएं", - "ShowDigitalRelease": "सिनेमा रिलीज की तारीख दिखाएँ" + "ShowDigitalRelease": "सिनेमा रिलीज की तारीख दिखाएँ", + "FolderNameTokens": "फ़ाइल नाम टोकन" } diff --git a/src/NzbDrone.Core/Localization/Core/hr.json b/src/NzbDrone.Core/Localization/Core/hr.json index b3d7a9b655..aba89e925b 100644 --- a/src/NzbDrone.Core/Localization/Core/hr.json +++ b/src/NzbDrone.Core/Localization/Core/hr.json @@ -21,7 +21,7 @@ "AddNewTmdbIdMessage": "Možeš pretraživati koristeći TMDb id filma. npr 'tmdb:71663'", "AddQualityProfile": "Dodaj Profil Kvalitete", "AddRestriction": "Dodaj Ograničenje", - "AddRootFolder": "asdf", + "AddRootFolder": "Dodaj Korijensku Mapu", "ApplicationUrlHelpText": "Vanjski URL ove aplikacije uključuje http(s)://, port i URL base", "ApplyTags": "Primjeni Oznake", "AptUpdater": "Koristi apt kako bi instalirao ažuriranje", @@ -117,7 +117,7 @@ "Add": "Dodaj", "Age": "Starost", "Agenda": "Agenda", - "AddRemotePathMapping": "Daljinsko Mapiranje Portova", + "AddRemotePathMapping": "Dodaj mapiranje mrežne putanje", "EditRemotePathMapping": "Daljinsko Mapiranje Portova", "Letterboxd": "Letterboxd", "Languages": "jezik", @@ -312,5 +312,47 @@ "DownloadClientSettingsRecentPriority": "Prioritet Klijenata", "DeleteRootFolderMessageText": "Jeste li sigurni da želite obrisati ovaj profil odgode?", "DeleteSelectedImportListExclusionsMessageText": "Jeste li sigurni da želite izbrisati ovu uvoznu listu isključenja?", - "DeleteSelectedCustomFormats": "Kloniraj Prilagođeni Format" + "DeleteSelectedCustomFormats": "Kloniraj Prilagođeni Format", + "AlternativeTitlesLoadError": "Neuspješno učitavanje alternativnih naslova.", + "AppUpdated": "{appName} Ažuriran", + "AuthenticationRequiredWarning": "Kako bi se spriječio udaljeni pristup bez autentikacije, {appName} sad zahtjeva da autentikacija bude omogućena. Izborno se može onemogućiti autentikacija s lokalnih adresa.", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Potvrdi novu lozinku", + "AddCondition": "Dodaj Uvjet", + "AddAutoTag": "Dodaj AutoOznaku", + "AllTitles": "Svi Naslovi", + "AddConditionImplementation": "Dodaj Uvjet - {implementationName}", + "AddConnection": "Dodaj vezu", + "AddConnectionImplementation": "Dodaj Vezu - {implementationName}", + "AddImportList": "Dodaj Listu Za Uvoz", + "AddImportListImplementation": "Dodaj Listu Za Uvoz - {implementationName}", + "AddIndexerImplementation": "Dodaj Indexer - {implementationName}", + "AddDownloadClientImplementation": "Dodaj Klijenta za Preuzimanje- {implementationName}", + "ApplyChanges": "Primjeni Promjene", + "AnnouncedMovieAvailabilityDescription": "Filmovi se smatraju dostupnim čim su dodani u {appName}.", + "AuthenticationRequired": "Potrebna Autentikacija", + "AudioLanguages": "Audio Jezici", + "AuthenticationRequiredPasswordHelpTextWarning": "Unesi novu lozinku", + "AuthenticationRequiredUsernameHelpTextWarning": "Unesi novo korisničko ime", + "AuthenticationMethod": "Metoda Autentikacije", + "AuthenticationMethodHelpTextWarning": "Molimo odaberi ispravnu metodu autentikacije", + "AutoRedownloadFailed": "Ponovno preuzimanje neuspješno", + "AutoRedownloadFailedFromInteractiveSearch": "Ponovno preuzimanje iz Interaktivne Pretrage neuspješno", + "ApiKeyValidationHealthCheckMessage": "Molimo ažuriraj svoj API ključ da ima barem {length} znakova. Ovo možeš uraditi u postavkama ili konfiguracijskoj datoteci", + "AddRootFolderError": "Neuspješno dodavanje korijenske mape", + "AnalyseVideoFilesHelpText": "Čitanje video informacija kao što su rezolucija, vrijeme izvođenja i kodek iz datoteka. Ovo zahtjeva da {appName} čita dijelove datoteke što može uzrokovati visoku aktivnost diska ili mreže tijekom skeniranja.", + "AddConditionError": "Neuspješno dodavanje novog uvjeta, molimo pokušaj ponovno.", + "AddAutoTagError": "Neuspješno dodavanje automatske oznake, molimo pokušaj ponovno.", + "AddImportListExclusionError": "Neuspješno dodavanje na listu za isključenje, molimo pokušaj ponovno.", + "AddIndexerError": "Neuspješno dodavanje novog indexera, molimo pokušaj ponovno.", + "AddListError": "Neuspješno dodavanje nove liste, molimo pokušaj ponovno.", + "AddNewRestriction": "Dodaj novo ograničenje", + "AddNotificationError": "Neuspješno dodavanje nove obavijesti, molimo pokušaj ponovno.", + "AddQualityProfileError": "Neuspješno dodavanje novog profila kvalitete, molimo pokušaj ponovno.", + "AddReleaseProfile": "Dodaj profil verzije", + "AddRemotePathMappingError": "Neuspješno dodavanje novog mapiranja mrežne putanje, molimo pokušaj ponovno.", + "AddCustomFormatError": "Neuspješno dodavanje novog prilagođenog formata, molimo pokušaj ponovno.", + "AddDelayProfileError": "Neuspješno dodavanje profila odgode, molimo pokušaj ponovno.", + "AddDownloadClientError": "Nesupješno dodavanje klijenta za preuzimanje, molimo pokušaj ponovno.", + "Any": "BIlo koji", + "AppUpdatedVersion": "{appName} je ažuriran na verziju '{version}', kako bi najnovije promjene bile aktivne potrebno je ponovno učitati {appName}" } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index dab5c61e3a..360416e1c9 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -1443,5 +1443,6 @@ "ShowPhysicalReleaseHelpText": "Mutasd a megjelenés dátumát a poszter alatt", "Logout": "Kijelentkezés", "SmartReplace": "Intelligens csere", - "SmartReplaceHint": "Dash vagy Space Dash névtől függően" + "SmartReplaceHint": "Dash vagy Space Dash névtől függően", + "FolderNameTokens": "Fájlnév-tokenek" } diff --git a/src/NzbDrone.Core/Localization/Core/is.json b/src/NzbDrone.Core/Localization/Core/is.json index ad7eddab88..dac8040927 100644 --- a/src/NzbDrone.Core/Localization/Core/is.json +++ b/src/NzbDrone.Core/Localization/Core/is.json @@ -1070,5 +1070,6 @@ "ShowDigitalRelease": "Sýna útgáfudag kvikmyndahússins", "ShowDigitalReleaseHelpText": "Sýnið útgáfudagsetningu undir veggspjaldi", "ShowPhysicalRelease": "Líkamlegur útgáfudagur", - "ShowPhysicalReleaseHelpText": "Sýnið útgáfudagsetningu undir veggspjaldi" + "ShowPhysicalReleaseHelpText": "Sýnið útgáfudagsetningu undir veggspjaldi", + "FolderNameTokens": "Auðkenni skráarheita" } diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index 9a7e2fa5ff..c766a1979b 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -1470,5 +1470,6 @@ "TomorrowAt": "Domani alle {time}", "YesterdayAt": "Ieri alle {time}", "MovieCollectionFolderMultipleMissingRootsHealthCheckMessage": "Diverse cartelle principale sono perse per l’importazione: {rootFoldersInfo}", - "ShowTraktRatingPosterHelpText": "Mostra valutazione Tomato sotta la locandina" + "ShowTraktRatingPosterHelpText": "Mostra valutazione Tomato sotta la locandina", + "FolderNameTokens": "Token nome file" } diff --git a/src/NzbDrone.Core/Localization/Core/ja.json b/src/NzbDrone.Core/Localization/Core/ja.json index 77c2a1b393..c02ec68e4e 100644 --- a/src/NzbDrone.Core/Localization/Core/ja.json +++ b/src/NzbDrone.Core/Localization/Core/ja.json @@ -1070,5 +1070,6 @@ "ShowDigitalRelease": "シネマのリリース日を表示", "ShowPhysicalRelease": "物理的なリリース日", "ShowPhysicalReleaseHelpText": "ポスターの下にリリース日を表示する", - "ShowDigitalReleaseHelpText": "ポスターの下にリリース日を表示する" + "ShowDigitalReleaseHelpText": "ポスターの下にリリース日を表示する", + "FolderNameTokens": "ファイル名トークン" } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index f2305a4fef..2f9909b224 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -1237,5 +1237,6 @@ "ShowDigitalRelease": "Show Cinema-releasedatum", "ShowDigitalReleaseHelpText": "Laat releasedatum zien onder poster", "ShowPhysicalRelease": "Fysieke Release Datum", - "ShowPhysicalReleaseHelpText": "Laat releasedatum zien onder poster" + "ShowPhysicalReleaseHelpText": "Laat releasedatum zien onder poster", + "FolderNameTokens": "Bestandsnaam Tokens" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 7ee96b9482..059f002f4f 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -1260,5 +1260,6 @@ "ShowDigitalReleaseHelpText": "Mostrar data de lançamento abaixo do cartaz", "ShowPhysicalRelease": "Data da versão física", "ShowPhysicalReleaseHelpText": "Mostrar data de lançamento abaixo do cartaz", - "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome" + "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome", + "FolderNameTokens": "Tokens de nome do ficheiro" } diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index dfdbf684fa..3a413b33c6 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -1835,5 +1835,6 @@ "InCinemasMovieAvailabilityDescription": "Filmes são considerados como disponíveis assim que entram em cartaz.", "ReleasedMovieAvailabilityDescription": "Filmes são considerados como disponíveis assim que é lançado em Blu-Ray ou no streaming.", "SmartReplace": "Substituição inteligente", - "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome" + "SmartReplaceHint": "Traço ou Espaço e Traço, dependendo do nome", + "FolderNameTokens": "Tokens de nome de arquivo" } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 06aa825ab7..2e6f09358a 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -1146,5 +1146,6 @@ "ShowDigitalRelease": "Data lansării Show Cinema", "ShowDigitalReleaseHelpText": "Afișați data lansării sub afiș", "ShowPhysicalRelease": "Data de lansare fizică", - "ShowPhysicalReleaseHelpText": "Afișați data lansării sub afiș" + "ShowPhysicalReleaseHelpText": "Afișați data lansării sub afiș", + "FolderNameTokens": "Jetoane cu nume de fișier" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index ecf5015b3b..4b5627d5fd 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -1776,5 +1776,6 @@ "LogSizeLimitHelpText": "Максимальный размер файла журнала в МБ перед архивированием. По умолчанию - 1 МБ.", "ShowTraktRatingPosterHelpText": "Показать рейтинг Tomato под постером", "SmartReplace": "Умная замена", - "SmartReplaceHint": "Тире или пробел в зависимости от имени" + "SmartReplaceHint": "Тире или пробел в зависимости от имени", + "FolderNameTokens": "Токены имени файла" } diff --git a/src/NzbDrone.Core/Localization/Core/th.json b/src/NzbDrone.Core/Localization/Core/th.json index b75e2146b5..5f350362ae 100644 --- a/src/NzbDrone.Core/Localization/Core/th.json +++ b/src/NzbDrone.Core/Localization/Core/th.json @@ -1068,5 +1068,6 @@ "ShowDigitalRelease": "วันฉายภาพยนตร์", "ShowDigitalReleaseHelpText": "แสดงวันที่วางจำหน่ายใต้โปสเตอร์", "ShowPhysicalRelease": "วันที่วางจำหน่ายจริง", - "ShowPhysicalReleaseHelpText": "แสดงวันที่วางจำหน่ายใต้โปสเตอร์" + "ShowPhysicalReleaseHelpText": "แสดงวันที่วางจำหน่ายใต้โปสเตอร์", + "FolderNameTokens": "โทเค็นชื่อไฟล์" } diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index 710547cadc..a3b4c5de6d 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -1827,5 +1827,6 @@ "Logout": "Çıkış", "NoBlocklistItems": "Engellenenler listesi öğesi yok", "LastSearched": "Son Aranan", - "ShowTraktRatingPosterHelpText": "Posterin altında Tomato derecelendirmesini göster" + "ShowTraktRatingPosterHelpText": "Posterin altında Tomato derecelendirmesini göster", + "FolderNameTokens": "Dosya Adı Belirteçleri" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index cbd40ae476..90b7947b09 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -1287,5 +1287,6 @@ "ShowDigitalRelease": "Показати дату виходу в кіно", "ShowDigitalReleaseHelpText": "Показати дату випуску під плакатом", "ShowPhysicalRelease": "Дата фізичного випуску", - "ShowPhysicalReleaseHelpText": "Показати дату випуску під плакатом" + "ShowPhysicalReleaseHelpText": "Показати дату випуску під плакатом", + "FolderNameTokens": "Маркери імен файлів" } diff --git a/src/NzbDrone.Core/Localization/Core/vi.json b/src/NzbDrone.Core/Localization/Core/vi.json index 88c6a9c51d..74becaf4ee 100644 --- a/src/NzbDrone.Core/Localization/Core/vi.json +++ b/src/NzbDrone.Core/Localization/Core/vi.json @@ -1074,5 +1074,6 @@ "ShowPhysicalReleaseHelpText": "Hiển thị ngày phát hành dưới áp phích", "ShowDigitalRelease": "Ngày phát hành rạp chiếu phim", "ShowDigitalReleaseHelpText": "Hiển thị ngày phát hành dưới áp phích", - "ShowPhysicalRelease": "Ngày phát hành vật lý" + "ShowPhysicalRelease": "Ngày phát hành vật lý", + "FolderNameTokens": "Mã thông báo tên tệp" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index 98eb80dfa6..4a0e1bfc02 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -1,5 +1,5 @@ { - "About": "关于关于", + "About": "关于", "DownloadClientCheckUnableToCommunicateMessage": "无法与{downloadClientName}进行通讯:{errorMessage}", "DownloadClientCheckNoneAvailableMessage": "无可用的下载客户端", "DownloadClient": "下载客户端", @@ -113,7 +113,7 @@ "CancelPendingTask": "您确定要取消这个挂起的任务吗?", "Peers": "用户", "AllowHardcodedSubs": "允许封装的字幕", - "YouCanAlsoSearch": "您同样可以使用TMDb ID或者IMDb ID搜索影片。例如:‘tmdb:71663’", + "YouCanAlsoSearch": "您也可以使用 TMDb ID 或 IMDb ID 搜索电影。例如:`tmdb:71663`", "Yesterday": "昨天", "Year": "年份", "Weeks": "周", @@ -121,7 +121,7 @@ "Username": "用户名", "UseProxy": "使用代理", "Uppercase": "大写字母", - "UnselectAll": "取消选择全部", + "UnselectAll": "取消全选", "UnsavedChanges": "未保存更改", "Unreleased": "未发布", "Unmonitored": "未追踪项", @@ -135,14 +135,14 @@ "ShowRatings": "显示评分", "ShowPath": "显示路径", "ShownClickToHide": "显示,点击隐藏", - "ShowMovieInformationHelpText": "显示影片风格和分级", - "ShowMovieInformation": "显示影片信息", + "ShowMovieInformationHelpText": "显示电影类型和分级信息", + "ShowMovieInformation": "显示电影信息", "ShowMonitored": "显示追踪状态", "ShowGenres": "显示类型", - "ShowDateAdded": "显示加入时间", - "ShowCertification": "显示分级", + "ShowDateAdded": "显示添加日期", + "ShowCertification": "显示分级信息", "ICalShowAsAllDayEvents": "作为全天事件显示", - "ShowAdvanced": "显示高级", + "ShowAdvanced": "高级设置", "TimeFormat": "时间格式", "ShortDateFormat": "短日期格式", "RemotePath": "远程路径", @@ -155,18 +155,18 @@ "SetPermissions": "设定权限", "SendAnonymousUsageData": "发送匿名使用数据", "SelectQuality": "选择质量", - "SelectMovie": "选择影片", + "SelectMovie": "选择电影", "SelectLanguage": "选择语言", "SelectFolder": "选择文件夹", - "SelectAll": "选择全部", + "SelectAll": "全选", "Seeders": "种子", "Security": "安全", "Seconds": "秒", "SearchSelected": "搜索已选", "SearchOnAdd": "添加时搜索", - "SearchMovie": "搜索影片", + "SearchMovie": "搜索电影", "SearchMissing": "搜索缺失项", - "SearchForMovie": "搜索影片", + "SearchForMovie": "搜索电影", "SearchForMissing": "搜索缺少", "SearchFailedPleaseTryAgainLater": "搜索失败,请稍后重试。", "SearchAll": "搜索全部", @@ -191,7 +191,7 @@ "Restart": "重启", "ResetAPIKey": "重置API Key", "Reset": "重置", - "RescanMovieFolderAfterRefresh": "刷新后重新扫描影片文件夹", + "RescanMovieFolderAfterRefresh": "刷新后重新扫描电影文件夹", "Required": "强制匹配", "ReplaceWithSpaceDashSpace": "使用空格破折号空格(xx - xx)替换", "ReplaceWithSpaceDash": "使用空格破折号(xx -xx)替换", @@ -205,8 +205,8 @@ "RemovingTag": "正在移除标签", "RemoveSelected": "移除选中项", "RemoveRootFolder": "移除根目录", - "RemoveMovieAndKeepFiles": "移除影片但保留文件", - "RemoveMovieAndDeleteFiles": "移除影片并删除文件", + "RemoveMovieAndKeepFiles": "移除电影但保留文件", + "RemoveMovieAndDeleteFiles": "移除电影并删除文件", "RemoveFromQueue": "从队列中移除", "RemoveFromDownloadClient": "从下载客户端中移除", "RemovedFromTaskQueue": "已从任务队列移除", @@ -224,7 +224,7 @@ "RecyclingBinCleanup": "清理回收站", "RecyclingBin": "回收站", "Ratings": "评分", - "QuickImport": "自动移动", + "QuickImport": "自动转移", "QueueIsEmpty": "空队列", "Queue": "队列", "PublishedDate": "发布日期", @@ -294,7 +294,7 @@ "Monday": "星期一", "MissingNotMonitored": "缺失中(未追踪)", "MissingMonitoredAndConsideredAvailable": "缺失中(已追踪)", - "Missing": "缺失", + "Missing": "缺失中", "MinutesSixty": "60分钟: {sixty}", "MinutesNinety": "90分钟: {ninety}", "MinutesHundredTwenty": "120分钟: {hundredTwenty}", @@ -334,8 +334,8 @@ "Indexer": "索引器", "InCinemasMovieDescription": "上映中电影", "InCinemasDate": "上映日期", - "InCinemas": "上映日期", - "ImportNotForDownloads": "不要使用该方法从下载客户端导入影片,本方法只限于导入现有的已整理的库,不能导入未整理的文件。", + "InCinemas": "已上映", + "ImportNotForDownloads": "请勿用于导入下载客户端的下载内容,本方法只限于导入已整理的现有资源库,不能导入未整理的文件。", "ImportMovies": "导入电影", "ImportMechanismHealthCheckMessage": "启用下载完成处理", "ImportListStatusCheckAllClientMessage": "所有的列表因错误不可用", @@ -449,7 +449,7 @@ "UnableToLoadRootFolders": "无法加载根目录", "RemotePathMappingsLoadError": "无法加载远程路径映射", "NamingSettingsLoadError": "无法加载命名设置", - "UnableToLoadMovies": "无法加载影片", + "UnableToLoadMovies": "无法加载电影", "MetadataLoadError": "无法加载元数据", "MediaManagementSettingsLoadError": "无法加载媒体管理设置", "UnableToLoadManualImportItems": "无法加载手动导入项目", @@ -495,8 +495,8 @@ "SslPort": "SSL端口", "SslCertPath": "SSL证书路径", "SourcePath": "来源路径", - "Source": "来源", - "SorryThatMovieCannotBeFound": "对不起,未找到影片。", + "Source": "代码", + "SorryThatMovieCannotBeFound": "对不起,未找到该电影。", "SkipFreeSpaceCheck": "跳过剩余空间检查", "SizeOnDisk": "占用磁盘体积", "Size": "大小", @@ -567,7 +567,7 @@ "MinimumCustomFormatScoreHelpText": "允许下载的最小自定义格式分数", "Min": "最小的", "MetadataSettingsMovieSummary": "导入或刷新电影时创建元数据文件", - "MaximumSizeHelpText": "抓取影片最大多少MB,设置为0则不限制", + "MaximumSizeHelpText": "抓取发布资源的最大大小(MB)。设置为零则不限制", "Max": "最大的", "MassMovieSearch": "批量搜索电影", "MarkAsFailedMessageText": "您确定要标记'{0}'为已失败?", @@ -580,7 +580,7 @@ "LoadingMovieExtraFilesFailed": "加载电影附加文件失败", "LoadingMovieCreditsFailed": "加载电影演职员表失败", "ListSyncLevelHelpTextWarning": "电影文件将被永久删除,如果您的列表为空,这可能会导致整个资源库被清空", - "ListSyncLevelHelpText": "如果资源库中的电影从您的列表中移除或从未出现在列表中,将根据您的选择进行处理。", + "ListSyncLevelHelpText": "如果资源库中的电影从您的列表中移除或从未出现在列表中,将根据您的选择对其进行处理", "LastWriteTime": "最后写入时间", "LastDuration": "上一次用时", "InstallLatest": "安装最新版", @@ -611,7 +611,7 @@ "GrabSelected": "抓取已选", "GrabRelease": "抓取版本", "GoToInterp": "跳转到 {0}", - "Genres": "风格", + "Genres": "类型", "FolderMoveRenameWarning": "这也将根据设置中的电影文件夹格式重命名电影文件夹。", "FocusSearchBox": "聚焦搜索框", "Fixed": "已修复", @@ -621,7 +621,7 @@ "ExportCustomFormat": "导出自定义格式", "ExistingTag": "已有标签", "ExistingMovies": "已有电影", - "ExcludeTitle": "排除{0}?这会防止{appName}从列表更新中自动添加该影片。", + "ExcludeTitle": "排除 {0} ?这会防止 {appName} 从列表同步中自动添加。", "ExcludeMovie": "排除的电影", "Excluded": "排除的", "Exception": "例外", @@ -662,8 +662,8 @@ "ChmodFolderHelpText": "八进制,当导入和重命名媒体文件夹和文件时应用(不带执行位)", "ChmodFolder": "修改文件夹权限", "BranchUpdateMechanism": "外部更新机制使用的分支", - "AvailabilityDelayHelpText": "在可用日期之前或之后搜索影片的总次数", - "AllowHardcodedSubsHelpText": "会自动下载检测到有硬字幕的影片", + "AvailabilityDelayHelpText": "在可用日期之前或之后搜索电影的时间范围", + "AllowHardcodedSubsHelpText": "检测到的硬编码字幕将自动下载", "AddRestriction": "添加限制", "AddDelayProfile": "添加延时配置", "UpgradesAllowed": "允许升级", @@ -690,7 +690,7 @@ "RecyclingBinHelpText": "电影文件将被移动至此以替代永久删除", "TableOptions": "表格选项", "UpdateSelected": "更新选择的内容", - "ShowUnknownMovieItems": "显示未知影片条目", + "ShowUnknownMovieItems": "显示未知电影项目", "RenameMoviesHelpText": "如果 “重命名电影” 未启用,{appName} 将使用现有文件名", "TorrentsDisabled": "Torrents关闭", "SomeResultsHiddenFilter": "部分结果已被过滤隐藏", @@ -707,7 +707,7 @@ "ProfilesSettingsSummary": "质量,语言,延迟和发布资源配置", "WeekColumnHeader": "日期格式", "YesCancel": "确定,取消", - "RescanAfterRefreshMovieHelpText": "刷新影片信息后重新扫描影片文件夹", + "RescanAfterRefreshMovieHelpText": "刷新电影信息后重新扫描电影文件夹", "Updates": "更新", "UnableToLoadRestrictions": "无法加载限制条件", "ICalIncludeUnmonitoredMoviesHelpText": "在 iCal 订阅中包含未追踪的电影", @@ -717,8 +717,8 @@ "Type": "类型", "ReleaseDates": "发布日期", "RemovedMovieCheckSingleMessage": "电影 “{movie}” 已从 TMDb 移除", - "UpgradeUntilThisQualityIsMetOrExceeded": "升级直到影片质量超出或者满足", - "ShowQualityProfileHelpText": "在海报下方显示媒体质量配置", + "UpgradeUntilThisQualityIsMetOrExceeded": "升级资源直至质量达标或高于标准", + "ShowQualityProfileHelpText": "在海报下方显示质量配置信息", "ReleaseRejected": "发布资源已拒绝", "UnmappedFilesOnly": "仅限未映射的文件", "Quality": "质量", @@ -748,10 +748,10 @@ "Overview": "概览", "RelativePath": "相对路径", "Large": "大", - "StandardMovieFormat": "标准影片格式", + "StandardMovieFormat": "标准电影格式", "UiSettingsSummary": "日历、日期和色弱模式选项", "Scheduled": "计划中", - "ShowQualityProfile": "显示质量配置文件", + "ShowQualityProfile": "显示质量配置", "IndexerRssHealthCheckNoAvailableIndexers": "由于最近索引器错误,所有支持 RSS 的索引器暂时不可用", "KeepAndUnmonitorMovie": "保留并取消追踪电影", "SupportedIndexers": "{appName} 支持任何使用 Newznab 标准的索引器,以及以下列出的其他搜刮器。", @@ -765,7 +765,7 @@ "ImportRootPath": "将 {appName} 指向包含所有电影的文件夹,而非某部特定电影的文件夹。例如:“{0}” 而非 “{1}”。此外,每部电影必须在「根/资源库」目录中有独立文件夹。", "TableOptionsColumnsMessage": "选择显示哪些列并排序", "Forecast": "预报表", - "ShowRelativeDatesHelpText": "显示相对日期(今天昨天等)或绝对日期", + "ShowRelativeDatesHelpText": "显示相对日期(今天/昨天等)或绝对日期", "ReleaseStatus": "发布状态", "RecentFolders": "最近文件夹", "ShowSearchHelpText": "悬停时显示搜索按钮", @@ -790,14 +790,14 @@ "DownloadPropersAndRepacks": "适合的和重封装的Propers and Repacks", "DoNotPrefer": "不要首选", "Reorder": "重新排序", - "MinimumAvailability": "最小可用性", + "MinimumAvailability": "最小可用条件", "MinimumAge": "最低间隔", "ListTagsHelpText": "标签列表项目将被添加和", "ExtraFileExtensionsHelpText": "导入逗号分隔其他文件(.nfo将做为.nfo-orig被导入)", "AvailabilityDelay": "可用性延迟", "AcceptConfirmationModal": "接受确认对话框", "Age": "年龄", - "CustomFormatHelpText": "{appName}会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数,有相同或更高的影片质量,则{appName}会抓取该发布版本。", + "CustomFormatHelpText": "{appName} 会根据满足自定义格式与否给每个发布版本评分,如果一个新的发布版本有更高的分数且有相同或更高的质量,则 {appName} 会抓取该发布版本。", "LookingForReleaseProfiles2": "代替。", "MountCheckMessage": "包含电影路径的挂载点被设置为只读: ", "NoAlternativeTitles": "没有其他标题。", @@ -864,7 +864,7 @@ "UsenetDisabled": "Usenet已关闭", "VisitTheWikiForMoreDetails": "访问wiki获取更多详细信息: ", "WaitingToProcess": "等待处理", - "Wanted": "待获取", + "Wanted": "待寻", "Warn": "警告", "Week": "周", "WhitelistedHardcodedSubsHelpText": "这里设置的字幕标签不会被认为是硬编码的", @@ -931,7 +931,7 @@ "GrabReleaseMessageText": "{appName} 无法确定此发布资源为何电影,{appName} 可能无法自动导入此资源,你想要抓取 “{0}” 吗?", "FeatureRequests": "功能建议", "Extension": "扩展", - "Discord": "分歧", + "Discord": "Discord", "CustomFormatUnknownConditionOption": "条件“{implementation}”的未知选项“{key}”", "Retention": "保留", "ChownGroupHelpText": "组名称或GID。对于远程文件系统请使用GID。", @@ -943,7 +943,7 @@ "QualityLimitsMovieRuntimeHelpText": "限制会根据电影的播放时长自动调整。", "QualitySettingsSummary": "质量标准和命名", "RetentionHelpText": "仅限Usenet:设置为零以设置无限保留", - "TorrentDelayHelpText": "延迟几分钟等待获取洪流", + "TorrentDelayHelpText": "抓取种子前需等待的延迟时间(分钟)", "UsenetDelayHelpText": "延迟几分钟才能等待从Usenet获取发布", "SqliteVersionCheckUpgradeRequiredMessage": "当前不再支持当前安装的SQLite版本{0}。请升级SQLite至少到版本{1}。", "ShowCinemaRelease": "显示上映日期", @@ -1019,9 +1019,9 @@ "DiscordUrlInSlackNotification": "你将Discord通知设为Slack通知,如设为Discord通知功能更好,受到影响的通知是: {0}", "Database": "数据库", "RefreshMonitoredIntervalHelpText": "从下载客户端刷新已追踪下载项的频率,最少 1 分钟", - "RssSyncIntervalHelpText": "以分钟间隔,设置为0关闭该功能(这会停止所有影片的自动抓取下载)", + "RssSyncIntervalHelpText": "间隔时间(分钟),设置为零则禁用(这会停止自动抓取发布资源)", "InstanceName": "中文", - "ShowCollectionDetails": "显示收藏状态", + "ShowCollectionDetails": "显示合集状态", "UnableToLoadCollections": "不能加载收藏", "Collections": "合集", "AllCollectionsHiddenDueToFilter": "由于应用了过滤器,所有合集已被隐藏。", @@ -1259,9 +1259,9 @@ "RemoveSelectedBlocklistMessageText": "您确认您想要从阻止列表中移除选中的项目吗?", "SelectDownloadClientModalTitle": "{modalTitle} - 选择下载客户端", "SetReleaseGroupModalTitle": "{modalTitle} - 设置发布组", - "ShowRottenTomatoesRating": "显示番茄评分", - "ShowRottenTomatoesRatingHelpText": "在海报下显示烂番茄评分", - "ShowTmdbRating": "显示TMDb评分", + "ShowRottenTomatoesRating": "显示烂番茄指数", + "ShowRottenTomatoesRatingHelpText": "在海报下显示烂番茄指数", + "ShowTmdbRating": "显示 TMDb 评分", "TableOptionsButton": "表格选项按钮", "TablePageSize": "页面大小", "TablePageSizeMaximum": "页面大小不得超过 {maximumValue}", @@ -1291,7 +1291,7 @@ "SubtitleLanguages": "字幕语言", "True": "是", "RootFolderPath": "根目录路径", - "ShowImdbRating": "显示IMDb评分", + "ShowImdbRating": "显示 IMDb 评分", "ShowImdbRatingHelpText": "在海报下显示 IMDb 评分", "AutoTaggingLoadError": "无法加载自动标记", "OverrideGrabNoMovie": "电影必须被选中", @@ -1672,9 +1672,9 @@ "DeleteSelectedCustomFormats": "删除自定义命名格式", "DeleteSelectedCustomFormatsMessageText": "您确定要删除选定的{count}导入列表吗?", "ReleaseDate": "发布日期", - "ShowDigitalRelease": "显示资源发布日期", - "ShowDigitalReleaseHelpText": "在海报下显示数字版资源发布日期", - "ShowPhysicalRelease": "碟片版发布日期", + "ShowDigitalRelease": "显示数字版发布日期", + "ShowDigitalReleaseHelpText": "在海报下显示数字版发布日期", + "ShowPhysicalRelease": "显示碟片版发布日期", "ShowPhysicalReleaseHelpText": "在海报下显示碟片版发布日期", "IncludePopular": "包含热门推荐", "IncludeTrending": "包含流行推荐", @@ -1826,7 +1826,15 @@ "MinimumCustomFormatScoreIncrementHelpText": "{appName} 将新版本视为升级版本之前,新版本资源相较于现有版本在自定义格式分数上的最小提升", "Recommended": "已推荐", "LastSearched": "最近搜索", - "ShowTraktRatingPosterHelpText": "在海报下显示烂番茄评分", + "ShowTraktRatingPosterHelpText": "在海报下显示 Trakt 评分", "SmartReplace": "智能替换", - "SmartReplaceHint": "短划线或空格短划线取决于名称" + "SmartReplaceHint": "短划线或空格短划线取决于名称", + "NotificationsGotifySettingsPreferredMetadataLinkHelpText": "仅支持单个链接的客户端的元数据链接", + "ShowTraktRating": "显示 Trakt 评分", + "TraktVotes": "Trakt 票数", + "AnnouncedMovieAvailabilityDescription": "一旦添加至 {appName} 便被视为可用的电影。", + "InCinemasMovieAvailabilityDescription": "一旦在影院上映便被视为可用的电影。", + "ReleasedMovieAvailabilityDescription": "一旦蓝光或流媒体版本发布便被视为可用的电影。", + "TraktRating": "Trakt 评分", + "FolderNameTokens": "文件名标记" } From a327b84a87fba1f11401310a530348d0d16783e7 Mon Sep 17 00:00:00 2001 From: Steel City Phantom Date: Tue, 8 Oct 2024 11:10:56 -0400 Subject: [PATCH 27/27] Update Directory.Build.props configured to auto-detect if building on an ARM mac or not --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 174d1cb8a2..1e722177f0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -221,7 +221,7 @@ <_UsingDefaultRuntimeIdentifier>true - osx-x64 + osx-$(Architecture)