From 803cda9d223264a51c750db540c7e40c1e56120e Mon Sep 17 00:00:00 2001 From: Uruk Date: Sun, 14 Apr 2024 14:23:22 +0200 Subject: [PATCH] Update to .NET 8 --- .devcontainer/devcontainer.json | 19 + .github/actions/package/package.sh | 2 +- .github/dependabot.yml | 12 + .github/workflows/build.yml | 30 +- .github/workflows/deploy.yml | 28 +- .gitignore | 1 + .vscode/extensions.json | 7 + .vscode/launch.json | 26 + .vscode/tasks.json | 44 + build.sh | 2 +- distribution/debian/install.sh | 1 + distribution/windows/setup/build.bat | 2 +- docs.sh | 2 +- frontend/src/App/State/AppSectionState.ts | 2 + frontend/src/App/State/SettingsAppState.ts | 11 + frontend/src/Commands/Command.ts | 2 + .../Components/Form/EnhancedSelectInput.css | 2 +- .../src/Components/Form/FormInputGroup.js | 4 + .../Form/MonitorEpisodesSelectInput.js | 8 +- .../Form/MonitorNewItemsSelectInput.js | 8 +- .../Components/Form/ProviderFieldFormGroup.js | 2 + .../QualityProfileSelectInputConnector.js | 4 +- .../src/Components/Form/SeriesTagInput.tsx | 53 ++ .../Components/Form/SeriesTypeSelectInput.tsx | 6 +- frontend/src/Components/Modal/Modal.css | 10 +- frontend/src/Components/Modal/Modal.css.d.ts | 1 + .../src/Content/Images/Icons/manifest.json | 2 +- frontend/src/Episode/EpisodeDetailsModal.js | 2 +- frontend/src/Episode/getReleaseTypeName.ts | 17 + frontend/src/EpisodeFile/EpisodeFile.ts | 2 + .../src/Helpers/Hooks/useModalOpenState.ts | 17 + frontend/src/Helpers/Props/inputTypes.js | 2 + frontend/src/Helpers/Props/sizes.js | 4 +- .../InteractiveImportModalContent.tsx | 41 +- .../Interactive/InteractiveImportRow.tsx | 44 +- .../InteractiveImport/InteractiveImport.ts | 3 + .../InteractiveImportModal.tsx | 2 +- frontend/src/InteractiveImport/ReleaseType.ts | 3 + .../ReleaseType/SelectReleaseTypeModal.tsx | 30 + .../SelectReleaseTypeModalContent.tsx | 99 +++ .../Select/Edit/EditSeriesModalContent.tsx | 4 +- .../Search/SeasonInteractiveSearchModal.js | 2 +- .../EditCustomFormatModalContent.js | 5 + .../EditDownloadClientModalContent.js | 9 + ...EditDownloadClientModalContentConnector.js | 17 +- .../ManageDownloadClientsEditModalContent.tsx | 2 +- .../EditImportListExclusionModal.js | 27 - .../EditImportListExclusionModal.tsx | 41 + .../EditImportListExclusionModalConnector.js | 43 - .../EditImportListExclusionModalContent.js | 139 --- .../EditImportListExclusionModalContent.tsx | 188 ++++ ...mportListExclusionModalContentConnector.js | 117 --- .../ImportListExclusion.css | 25 - .../ImportListExclusion.css.d.ts | 3 - .../ImportListExclusion.js | 112 --- .../ImportListExclusionRow.css | 6 + .../ImportListExclusionRow.css.d.ts | 7 + .../ImportListExclusionRow.tsx | 68 ++ .../ImportListExclusions.css | 23 - .../ImportListExclusions.css.d.ts | 3 - .../ImportListExclusions.js | 105 --- .../ImportListExclusions.tsx | 232 +++++ .../ImportListExclusionsConnector.js | 59 -- .../ImportLists/ImportListSettings.js | 4 +- .../ImportLists/EditImportListModalContent.js | 9 + .../EditImportListModalContentConnector.js | 17 +- .../ManageImportListsEditModalContent.tsx | 2 +- .../Edit/ManageIndexersEditModalContent.tsx | 2 +- .../MediaManagement/Naming/NamingModal.js | 80 +- .../MediaManagement/Naming/NamingOption.css | 4 +- .../src/Settings/MetadataSource/TheTvdb.js | 1 - .../EditNotificationModalContent.js | 9 + .../EditNotificationModalContentConnector.js | 17 +- frontend/src/Settings/Tags/TagInUse.js | 2 +- .../Actions/Settings/importListExclusions.js | 43 +- .../Store/Actions/interactiveImportActions.js | 1 + frontend/src/Store/Actions/releaseActions.js | 6 + frontend/src/Store/Actions/settingsActions.js | 5 +- .../Selectors/createMultiSeriesSelector.ts | 23 + .../createSettingsSectionSelector.ts | 51 +- .../src/System/Tasks/Queued/QueuedTaskRow.css | 9 - .../Tasks/Queued/QueuedTaskRow.css.d.ts | 2 - .../src/System/Tasks/Queued/QueuedTaskRow.js | 279 ------ .../src/System/Tasks/Queued/QueuedTaskRow.tsx | 238 +++++ .../Tasks/Queued/QueuedTaskRowConnector.js | 31 - .../Tasks/Queued/QueuedTaskRowNameCell.css | 8 + .../Queued/QueuedTaskRowNameCell.css.d.ts | 8 + .../Tasks/Queued/QueuedTaskRowNameCell.tsx | 57 ++ .../src/System/Tasks/Queued/QueuedTasks.js | 90 -- .../src/System/Tasks/Queued/QueuedTasks.tsx | 74 ++ .../Tasks/Queued/QueuedTasksConnector.js | 46 - frontend/src/System/Tasks/Tasks.js | 4 +- frontend/src/index.ejs | 7 +- frontend/src/login.html | 7 +- frontend/src/typings/ImportListExclusion.ts | 6 + frontend/src/typings/pending.ts | 18 +- global.json | 2 +- package.json | 4 +- src/Directory.Build.props | 55 +- src/NzbDrone.Api.Test/Sonarr.Api.Test.csproj | 2 +- .../Sonarr.Automation.Test.csproj | 6 +- src/NzbDrone.Automation.Test/app.config | 2 +- .../CleanseLogMessageFixture.cs | 2 + .../Sonarr.Common.Test.csproj | 2 +- .../Disk/DestinationAlreadyExistsException.cs | 6 - .../EnvironmentInfo/RuntimeInfo.cs | 2 +- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 40 +- .../Instrumentation/CleanseLogMessage.cs | 1 + src/NzbDrone.Common/Sonarr.Common.csproj | 22 +- src/NzbDrone.Console/Sonarr.Console.csproj | 2 +- src/NzbDrone.Core.Test/Framework/CoreTest.cs | 2 +- .../Housekeepers/CleanupUnusedTagsFixture.cs | 33 + .../ImportListSyncServiceFixture.cs | 32 + .../AggregateReleaseHashFixture.cs | 83 ++ .../AggregateSubtitleInfoFixture.cs | 19 +- .../CustomFormatsFixture.cs | 146 ++++ .../FileNameBuilderFixture.cs | 22 + .../AbsoluteEpisodeNumberParserFixture.cs | 4 + .../ParserTests/AnimeMetadataParserFixture.cs | 32 +- .../ParserTests/LanguageParserFixture.cs | 1 + .../ParserTests/MultiEpisodeParserFixture.cs | 1 + .../ParserTests/PathParserFixture.cs | 5 + .../ParserTests/ReleaseGroupParserFixture.cs | 1 + .../Sonarr.Core.Test.csproj | 4 +- .../HandleEpisodeFileDeletedFixture.cs | 36 +- .../Annotations/FieldDefinitionAttribute.cs | 3 +- .../Specifications/TagSpecification.cs | 36 + .../CustomFormats/CustomFormat.cs | 2 +- ...ication.cs => ReleaseTypeSpecification.cs} | 4 +- .../Migration/204_add_release_hash.cs | 76 ++ .../Migration/205_rename_season_pack_spec.cs | 14 + .../Download/Clients/Deluge/DelugeProxy.cs | 42 +- .../Download/Clients/Deluge/DelugeSettings.cs | 6 + .../Clients/QBittorrent/QBittorrent.cs | 12 +- .../Download/Clients/rTorrent/RTorrent.cs | 4 +- .../Extras/ExistingExtraFileService.cs | 8 +- .../Extras/IImportExistingExtraFiles.cs | 2 +- .../Extras/ImportExistingExtraFilesBase.cs | 13 +- .../Metadata/ExistingMetadataImporter.cs | 4 +- .../Others/ExistingOtherExtraImporter.cs | 4 +- .../Subtitles/ExistingSubtitleImporter.cs | 16 +- .../Checks/DownloadClientRootFolderCheck.cs | 2 +- .../Housekeepers/CleanupUnusedTags.cs | 36 +- .../ImportLists/AniList/List/AniListImport.cs | 60 ++ .../Exclusions/ImportListExclusionService.cs | 7 + .../ImportLists/ImportListSyncService.cs | 25 +- .../MyAnimeList/MyAnimeListImport.cs | 121 +++ .../MyAnimeList/MyAnimeListParser.cs | 31 + .../MyAnimeListRequestGenerator.cs | 50 ++ .../MyAnimeList/MyAnimeListResponses.cs | 55 ++ .../MyAnimeList/MyAnimeListSettings.cs | 58 ++ .../MyAnimeList/MyAnimeListStatus.cs | 25 + .../IndexerSearch/ReleaseSearchService.cs | 2 +- src/NzbDrone.Core/Indexers/RssSyncCommand.cs | 1 - src/NzbDrone.Core/Localization/Core/ca.json | 60 +- src/NzbDrone.Core/Localization/Core/cs.json | 6 +- src/NzbDrone.Core/Localization/Core/da.json | 19 +- src/NzbDrone.Core/Localization/Core/de.json | 70 +- src/NzbDrone.Core/Localization/Core/el.json | 4 +- src/NzbDrone.Core/Localization/Core/en.json | 21 +- src/NzbDrone.Core/Localization/Core/es.json | 814 ++++++++++++++++-- src/NzbDrone.Core/Localization/Core/fi.json | 38 +- src/NzbDrone.Core/Localization/Core/fr.json | 495 +++++++---- src/NzbDrone.Core/Localization/Core/hu.json | 18 +- src/NzbDrone.Core/Localization/Core/it.json | 8 +- src/NzbDrone.Core/Localization/Core/ko.json | 2 +- .../Localization/Core/nb_NO.json | 9 +- src/NzbDrone.Core/Localization/Core/nl.json | 41 +- src/NzbDrone.Core/Localization/Core/pt.json | 8 +- .../Localization/Core/pt_BR.json | 36 +- src/NzbDrone.Core/Localization/Core/ro.json | 7 +- src/NzbDrone.Core/Localization/Core/ru.json | 11 +- src/NzbDrone.Core/Localization/Core/sk.json | 42 +- src/NzbDrone.Core/Localization/Core/tr.json | 84 +- src/NzbDrone.Core/Localization/Core/uk.json | 68 +- .../Localization/Core/zh_CN.json | 26 +- .../MediaFiles/DiskScanService.cs | 12 +- src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 1 + .../MediaFiles/EpisodeFileMovingService.cs | 10 +- .../Aggregators/AggregateReleaseHash.cs | 41 + .../Aggregators/AggregateSubtitleInfo.cs | 12 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 6 +- .../Manual/ManualImportService.cs | 11 +- .../Specifications/FullSeasonSpecification.cs | 4 +- .../MediaFiles/MediaFileDeletionService.cs | 34 +- .../Messaging/Commands/Command.cs | 2 +- .../MetadataSource/ISearchForNewSeries.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 7 + .../Notifications/NotificationService.cs | 26 +- .../Notifications/Ntfy/NtfyProxy.cs | 11 +- .../Plex/PlexTv/PlexTvService.cs | 5 +- .../Notifications/Telegram/Telegram.cs | 36 +- .../Notifications/Telegram/TelegramProxy.cs | 3 +- .../Telegram/TelegramSettings.cs | 3 + .../Notifications/Webhook/WebhookBase.cs | 2 + .../Webhook/WebhookDownloadStatusMessage.cs | 18 + .../WebhookManualInteractionPayload.cs | 2 + .../Notifications/Xbmc/XbmcJsonApiProxy.cs | 2 +- .../Notifications/Xbmc/XbmcSettings.cs | 6 +- .../Organizer/FileNameBuilder.cs | 48 +- .../Organizer/FileNameValidation.cs | 3 + .../Organizer/FileNameValidationService.cs | 15 +- src/NzbDrone.Core/Parser/LanguageParser.cs | 2 +- .../Parser/Model/LocalEpisode.cs | 3 +- .../Parser/Model/ParsedEpisodeInfo.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 106 ++- .../ProgressMessageContext.cs | 16 +- src/NzbDrone.Core/Sonarr.Core.csproj | 26 +- src/NzbDrone.Core/Tags/TagService.cs | 23 +- src/NzbDrone.Core/Tv/AddSeriesService.cs | 8 +- .../Tv/Commands/RefreshSeriesCommand.cs | 2 + src/NzbDrone.Core/Tv/EpisodeService.cs | 104 ++- src/NzbDrone.Core/Tv/MoveSeriesService.cs | 7 + .../Commands/ApplicationUpdateCommand.cs | 2 - .../Sonarr.Host.Test.csproj | 2 +- src/NzbDrone.Host/Sonarr.Host.csproj | 12 +- .../Sonarr.Integration.Test.csproj | 4 +- .../Sonarr.Libraries.Test.csproj | 2 +- src/NzbDrone.Libraries.Test/app.config | 2 +- .../Sonarr.Mono.Test.csproj | 2 +- src/NzbDrone.Mono.Test/app.config | 2 +- src/NzbDrone.Mono/Disk/FindDriveType.cs | 19 +- src/NzbDrone.Mono/Sonarr.Mono.csproj | 2 +- src/NzbDrone.SignalR/Sonarr.SignalR.csproj | 2 +- src/NzbDrone.Test.Common/App.config | 2 +- src/NzbDrone.Test.Common/NzbDroneRunner.cs | 2 +- .../Sonarr.Test.Common.csproj | 14 +- .../Sonarr.Test.Dummy.csproj | 2 +- src/NzbDrone.Test.Dummy/app.config | 5 +- .../Sonarr.Update.Test.csproj | 2 +- src/NzbDrone.Update/Sonarr.Update.csproj | 6 +- src/NzbDrone.Update/app.config | 2 +- .../Sonarr.Windows.Test.csproj | 2 +- src/NzbDrone.Windows.Test/app.config | 2 +- src/NzbDrone.Windows/Sonarr.Windows.csproj | 4 +- src/NzbDrone/Sonarr.csproj | 4 +- .../ServiceInstall/ServiceInstall.csproj | 2 +- src/ServiceHelpers/ServiceInstall/app.config | 2 +- .../ServiceUninstall/ServiceUninstall.csproj | 2 +- .../ServiceUninstall/app.config | 2 +- .../Commands/CommandController.cs | 7 +- .../EpisodeFiles/EpisodeFileResource.cs | 5 +- .../ImportListExclusionController.cs | 13 + .../Indexers/ReleasePushController.cs | 5 +- .../ManualImport/ManualImportController.cs | 3 +- .../ManualImportReprocessResource.cs | 2 + src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 6 +- src/Sonarr.Api.V3/openapi.json | 115 ++- .../ApiKeyAuthenticationHandler.cs | 3 +- .../BasicAuthenticationHandler.cs | 8 +- .../Authentication/NoAuthenticationHandler.cs | 5 +- .../Middleware/VersionMiddleware.cs | 2 +- src/Sonarr.Http/Ping/PingController.cs | 1 + src/Sonarr.Http/REST/RestController.cs | 3 +- src/Sonarr.Http/Sonarr.Http.csproj | 8 +- .../Sonarr.RuntimePatches.csproj | 4 +- src/coverlet.runsettings | 2 +- test.sh | 1 + yarn.lock | 38 +- 259 files changed, 5140 insertions(+), 1945 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/dependabot.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 frontend/src/Components/Form/SeriesTagInput.tsx create mode 100644 frontend/src/Episode/getReleaseTypeName.ts create mode 100644 frontend/src/Helpers/Hooks/useModalOpenState.ts create mode 100644 frontend/src/InteractiveImport/ReleaseType.ts create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx create mode 100644 frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx delete mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js create mode 100644 frontend/src/Store/Selectors/createMultiSeriesSelector.ts delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRow.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts create mode 100644 frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTasks.js create mode 100644 frontend/src/System/Tasks/Queued/QueuedTasks.tsx delete mode 100644 frontend/src/System/Tasks/Queued/QueuedTasksConnector.js create mode 100644 frontend/src/typings/ImportListExclusion.ts create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs create mode 100644 src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs create mode 100644 src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs rename src/NzbDrone.Core/CustomFormats/Specifications/{SeasonPackSpecification.cs => ReleaseTypeSpecification.cs} (92%) create mode 100644 src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs create mode 100644 src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs create mode 100644 src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs create mode 100644 src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..629a2aa21 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "Sonarr", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "nodeGypDependencies": true, + "version": "16", + "nvmVersion": "latest" + } + }, + "forwardPorts": [8989], + "customizations": { + "vscode": { + "extensions": ["esbenp.prettier-vscode"] + } + } +} diff --git a/.github/actions/package/package.sh b/.github/actions/package/package.sh index 8dce60585..4c3c8a8ba 100755 --- a/.github/actions/package/package.sh +++ b/.github/actions/package/package.sh @@ -3,7 +3,7 @@ outputFolder=_output artifactsFolder=_artifacts uiFolder="$outputFolder/UI" -framework="${FRAMEWORK:=net6.0}" +framework="${FRAMEWORK:=net8.0}" rm -rf $artifactsFolder mkdir $artifactsFolder diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f33a02cd1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c65b49f2..e0c23a88c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ concurrency: cancel-in-progress: true env: - FRAMEWORK: net6.0 + FRAMEWORK: net8.0 RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }} SONARR_MAJOR_VERSION: 4 - VERSION: 4.0.2 + VERSION: 4.0.4 jobs: backend: @@ -121,7 +121,7 @@ jobs: run: yarn lint - name: Stylelint - run: yarn stylelint + run: yarn stylelint -f github - name: Build run: yarn build --env production @@ -217,7 +217,7 @@ jobs: deploy: if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }} - needs: [backend, unit_test, unit_test_postgres, integration_test] + needs: [backend, frontend, unit_test, unit_test_postgres, integration_test] secrets: inherit uses: ./.github/workflows/deploy.yml with: @@ -225,3 +225,25 @@ jobs: branch: ${{ github.ref_name }} major_version: ${{ needs.backend.outputs.major_version }} version: ${{ needs.backend.outputs.version }} + + notify: + name: Discord Notification + needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy] + if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }} + env: + STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }} + runs-on: ubuntu-latest + + steps: + - name: Notify + uses: tsickert/discord-webhook@v5.3.0 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + username: 'GitHub Actions' + avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png' + embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}" + embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' + embed-description: | + **Branch** ${{ github.ref }} + **Build** ${{ needs.backend.outputs.version }} + embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c477cd8b9..4fa5b54ee 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,12 +69,38 @@ jobs: pattern: release_* merge-multiple: true + - name: Get Previous Release + id: previous-release + uses: cardinalby/git-get-release-action@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + latest: true + prerelease: ${{ inputs.branch != 'main' }} + + - name: Generate Release Notes + id: generate-release-notes + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + result-encoding: string + script: | + const { data } = await github.rest.repos.generateReleaseNotes({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: 'v${{ inputs.version }}', + target_commitish: '${{ github.sha }}', + previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}', + }) + return data.body + - name: Create release uses: ncipollo/release-action@v1 with: artifacts: _artifacts/Sonarr.* commit: ${{ github.sha }} - generateReleaseNotes: true + generateReleaseNotes: false + body: ${{ steps.generate-release-notes.outputs.result }} name: ${{ inputs.version }} prerelease: ${{ inputs.branch != 'main' }} skipIfReleaseExists: true diff --git a/.gitignore b/.gitignore index 73bd6ad62..4094c46a6 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ coverage*.xml coverage*.json setup/Output/ *.~is +.mono #VS outout folders bin diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..7a36fefe1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "ms-dotnettools.csdevkit", + "ms-vscode-remote.remote-containers" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..6ea80f418 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": "Run Sonarr", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build dotnet", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/_output/net6.0/Sonarr", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..cfd41d42f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build dotnet", + "command": "dotnet", + "type": "process", + "args": [ + "msbuild", + "-restore", + "${workspaceFolder}/src/Sonarr.sln", + "-p:GenerateFullPaths=true", + "-p:Configuration=Debug", + "-p:Platform=Posix", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/Sonarr.sln", + "-property:GenerateFullPaths=true", + "-consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/Sonarr.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/build.sh b/build.sh index ee59d2f61..44e86f8f2 100755 --- a/build.sh +++ b/build.sh @@ -4,7 +4,7 @@ set -e outputFolder='_output' testPackageFolder='_tests' artifactsFolder="_artifacts"; -framework="${FRAMEWORK:=net6.0}" +framework="${FRAMEWORK:=net8.0}" ProgressStart() { diff --git a/distribution/debian/install.sh b/distribution/debian/install.sh index 87a0b0914..303b56702 100644 --- a/distribution/debian/install.sh +++ b/distribution/debian/install.sh @@ -1,4 +1,5 @@ #!/bin/bash + ### Description: Sonarr .NET Debian install ### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0 ### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN diff --git a/distribution/windows/setup/build.bat b/distribution/windows/setup/build.bat index 6c205089e..0d6b89d23 100644 --- a/distribution/windows/setup/build.bat +++ b/distribution/windows/setup/build.bat @@ -1,7 +1,7 @@ @REM SET SONARR_MAJOR_VERSION=4 @REM SET SONARR_VERSION=4.0.0.5 @REM SET BRANCH=develop -@REM SET FRAMEWORK=net6.0 +@REM SET FRAMEWORK=net8.0 @REM SET RUNTIME=win-x64 inno\ISCC.exe sonarr.iss diff --git a/docs.sh b/docs.sh index a0f21c41a..91533eed7 100755 --- a/docs.sh +++ b/docs.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -FRAMEWORK="net6.0" +FRAMEWORK="net8.0" PLATFORM=$1 if [ "$PLATFORM" = "Windows" ]; then diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index cabc39b1c..30af90d34 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -19,6 +19,7 @@ export interface AppSectionSaveState { export interface PagedAppSectionState { pageSize: number; + totalRecords?: number; } export interface AppSectionFilterState { @@ -38,6 +39,7 @@ export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; error: Error; + pendingChanges: Partial; item: T; } diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index a0bea0973..e4322db69 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -3,10 +3,12 @@ import AppSectionState, { AppSectionItemState, AppSectionSaveState, AppSectionSchemaState, + PagedAppSectionState, } from 'App/State/AppSectionState'; import Language from 'Language/Language'; import DownloadClient from 'typings/DownloadClient'; import ImportList from 'typings/ImportList'; +import ImportListExclusion from 'typings/ImportListExclusion'; import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; @@ -41,6 +43,14 @@ export interface ImportListOptionsSettingsAppState extends AppSectionItemState, AppSectionSaveState {} +export interface ImportListExclusionsSettingsAppState + extends AppSectionState, + AppSectionSaveState, + PagedAppSectionState, + AppSectionDeleteState { + pendingChanges: Partial; +} + export type IndexerFlagSettingsAppState = AppSectionState; export type LanguageSettingsAppState = AppSectionState; export type UiSettingsAppState = AppSectionItemState; @@ -48,6 +58,7 @@ export type UiSettingsAppState = AppSectionItemState; interface SettingsAppState { advancedSettings: boolean; downloadClients: DownloadClientAppState; + importListExclusions: ImportListExclusionsSettingsAppState; importListOptions: ImportListOptionsSettingsAppState; importLists: ImportListAppState; indexerFlags: IndexerFlagSettingsAppState; diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index 45a5beed7..0830fd34b 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -13,6 +13,8 @@ export interface CommandBody { trigger: string; suppressMessages: boolean; seriesId?: number; + seriesIds?: number[]; + seasonNumber?: number; } interface Command extends ModelBase { diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css index 56f5564b9..defefb18e 100644 --- a/frontend/src/Components/Form/EnhancedSelectInput.css +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -19,7 +19,7 @@ .isDisabled { opacity: 0.7; - cursor: not-allowed; + cursor: not-allowed !important; } .dropdownArrowContainer { diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index f7b2ce75e..7a3191cdc 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -22,6 +22,7 @@ import PasswordInput from './PasswordInput'; import PathInputConnector from './PathInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; +import SeriesTagInput from './SeriesTagInput'; import SeriesTypeSelectInput from './SeriesTypeSelectInput'; import TagInputConnector from './TagInputConnector'; import TagSelectInputConnector from './TagSelectInputConnector'; @@ -87,6 +88,9 @@ function getComponent(type) { case inputTypes.DYNAMIC_SELECT: return EnhancedSelectInputConnector; + case inputTypes.SERIES_TAG: + return SeriesTagInput; + case inputTypes.SERIES_TYPE_SELECT: return SeriesTypeSelectInput; diff --git a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js index 9b80cc587..a4ee4fd85 100644 --- a/frontend/src/Components/Form/MonitorEpisodesSelectInput.js +++ b/frontend/src/Components/Form/MonitorEpisodesSelectInput.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import monitorOptions from 'Utilities/Series/monitorOptions'; import translate from 'Utilities/String/translate'; -import SelectInput from './SelectInput'; +import EnhancedSelectInput from './EnhancedSelectInput'; function MonitorEpisodesSelectInput(props) { const { @@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) { get value() { return translate('NoChange'); }, - disabled: true + isDisabled: true }); } @@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) { get value() { return `(${translate('Mixed')})`; }, - disabled: true + isDisabled: true }); } return ( - diff --git a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js index c704e5c1f..be179c3e5 100644 --- a/frontend/src/Components/Form/MonitorNewItemsSelectInput.js +++ b/frontend/src/Components/Form/MonitorNewItemsSelectInput.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions'; -import SelectInput from './SelectInput'; +import EnhancedSelectInput from './EnhancedSelectInput'; function MonitorNewItemsSelectInput(props) { const { @@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'noChange', value: 'No Change', - disabled: true + isDisabled: true }); } @@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) { values.unshift({ key: 'mixed', value: '(Mixed)', - disabled: true + isDisabled: true }); } return ( - diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index a184aa1ec..4fcf99cc0 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) { return inputTypes.DYNAMIC_SELECT; } return inputTypes.SELECT; + case 'seriesTag': + return inputTypes.SERIES_TAG; case 'tag': return inputTypes.TEXT_TAG; case 'tagSelect': diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index cc8ffbdb8..48fc6bc35 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -28,7 +28,7 @@ function createMapStateToProps() { get value() { return translate('NoChange'); }, - disabled: includeNoChangeDisabled + isDisabled: includeNoChangeDisabled }); } @@ -38,7 +38,7 @@ function createMapStateToProps() { get value() { return `(${translate('Mixed')})`; }, - disabled: true + isDisabled: true }); } diff --git a/frontend/src/Components/Form/SeriesTagInput.tsx b/frontend/src/Components/Form/SeriesTagInput.tsx new file mode 100644 index 000000000..3d8279aa6 --- /dev/null +++ b/frontend/src/Components/Form/SeriesTagInput.tsx @@ -0,0 +1,53 @@ +import React, { useCallback } from 'react'; +import TagInputConnector from './TagInputConnector'; + +interface SeriesTageInputProps { + name: string; + value: number | number[]; + onChange: ({ + name, + value, + }: { + name: string; + value: number | number[]; + }) => void; +} + +export default function SeriesTagInput(props: SeriesTageInputProps) { + const { value, onChange, ...otherProps } = props; + const isArray = Array.isArray(value); + + const handleChange = useCallback( + ({ name, value: newValue }: { name: string; value: number[] }) => { + if (isArray) { + onChange({ name, value: newValue }); + } else { + onChange({ + name, + value: newValue.length ? newValue[newValue.length - 1] : 0, + }); + } + }, + [isArray, onChange] + ); + + let finalValue: number[] = []; + + if (isArray) { + finalValue = value; + } else if (value === 0) { + finalValue = []; + } else { + finalValue = [value]; + } + + return ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore 2786 'TagInputConnector' isn't typed yet + + ); +} diff --git a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx index 471d6592b..cea7f4fb5 100644 --- a/frontend/src/Components/Form/SeriesTypeSelectInput.tsx +++ b/frontend/src/Components/Form/SeriesTypeSelectInput.tsx @@ -15,7 +15,7 @@ interface ISeriesTypeOption { key: string; value: string; format?: string; - disabled?: boolean; + isDisabled?: boolean; } const seriesTypeOptions: ISeriesTypeOption[] = [ @@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { values.unshift({ key: 'noChange', value: translate('NoChange'), - disabled: includeNoChangeDisabled, + isDisabled: includeNoChangeDisabled, }); } @@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) { values.unshift({ key: 'mixed', value: `(${translate('Mixed')})`, - disabled: true, + isDisabled: true, }); } diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css index 33f849945..f7a229501 100644 --- a/frontend/src/Components/Modal/Modal.css +++ b/frontend/src/Components/Modal/Modal.css @@ -63,6 +63,13 @@ width: 1280px; } + +.extraExtraLarge { + composes: modal; + + width: 1600px; +} + @media only screen and (max-width: $breakpointExtraLarge) { .modal.extraLarge { width: 90%; @@ -90,7 +97,8 @@ .modal.small, .modal.medium, .modal.large, - .modal.extraLarge { + .modal.extraLarge, + .modal.extraExtraLarge { max-height: 100%; width: 100%; height: 100% !important; diff --git a/frontend/src/Components/Modal/Modal.css.d.ts b/frontend/src/Components/Modal/Modal.css.d.ts index b6576c7de..e582ce0f9 100644 --- a/frontend/src/Components/Modal/Modal.css.d.ts +++ b/frontend/src/Components/Modal/Modal.css.d.ts @@ -1,6 +1,7 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'extraExtraLarge': string; 'extraLarge': string; 'large': string; 'medium': string; diff --git a/frontend/src/Content/Images/Icons/manifest.json b/frontend/src/Content/Images/Icons/manifest.json index 9da62fe58..c7bd44495 100644 --- a/frontend/src/Content/Images/Icons/manifest.json +++ b/frontend/src/Content/Images/Icons/manifest.json @@ -15,5 +15,5 @@ "start_url": "../../../../", "theme_color": "#3a3f51", "background_color": "#3a3f51", - "display": "minimal-ui" + "display": "standalone" } diff --git a/frontend/src/Episode/EpisodeDetailsModal.js b/frontend/src/Episode/EpisodeDetailsModal.js index cd2e20e74..0e9583e3a 100644 --- a/frontend/src/Episode/EpisodeDetailsModal.js +++ b/frontend/src/Episode/EpisodeDetailsModal.js @@ -37,7 +37,7 @@ class EpisodeDetailsModal extends Component { return ( diff --git a/frontend/src/Episode/getReleaseTypeName.ts b/frontend/src/Episode/getReleaseTypeName.ts new file mode 100644 index 000000000..a2bb1af5b --- /dev/null +++ b/frontend/src/Episode/getReleaseTypeName.ts @@ -0,0 +1,17 @@ +import ReleaseType from 'InteractiveImport/ReleaseType'; +import translate from 'Utilities/String/translate'; + +export default function getReleaseTypeName( + releaseType?: ReleaseType +): string | null { + switch (releaseType) { + case 'singleEpisode': + return translate('SingleEpisode'); + case 'multiEpisode': + return translate('MultiEpisode'); + case 'seasonPack': + return translate('SeasonPack'); + default: + return translate('Unknown'); + } +} diff --git a/frontend/src/EpisodeFile/EpisodeFile.ts b/frontend/src/EpisodeFile/EpisodeFile.ts index 53dd53750..da362db82 100644 --- a/frontend/src/EpisodeFile/EpisodeFile.ts +++ b/frontend/src/EpisodeFile/EpisodeFile.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import ReleaseType from 'InteractiveImport/ReleaseType'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; @@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase { quality: QualityModel; customFormats: CustomFormat[]; indexerFlags: number; + releaseType: ReleaseType; mediaInfo: MediaInfo; qualityCutoffNotMet: boolean; } diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts new file mode 100644 index 000000000..f5b5a96f0 --- /dev/null +++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts @@ -0,0 +1,17 @@ +import { useCallback, useState } from 'react'; + +export default function useModalOpenState( + initialState: boolean +): [boolean, () => void, () => void] { + const [isOpen, setOpen] = useState(initialState); + + const setModalOpen = useCallback(() => { + setOpen(true); + }, [setOpen]); + + const setModalClosed = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return [isOpen, setModalOpen, setModalClosed]; +} diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js index dcf4b539c..a71c28d8c 100644 --- a/frontend/src/Helpers/Props/inputTypes.js +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -17,6 +17,7 @@ export const LANGUAGE_SELECT = 'languageSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const SELECT = 'select'; +export const SERIES_TAG = 'seriesTag'; export const DYNAMIC_SELECT = 'dynamicSelect'; export const SERIES_TYPE_SELECT = 'seriesTypeSelect'; export const TAG = 'tag'; @@ -45,6 +46,7 @@ export const all = [ ROOT_FOLDER_SELECT, LANGUAGE_SELECT, SELECT, + SERIES_TAG, DYNAMIC_SELECT, SERIES_TYPE_SELECT, TAG, diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js index d7f85df5e..6ac15f3bd 100644 --- a/frontend/src/Helpers/Props/sizes.js +++ b/frontend/src/Helpers/Props/sizes.js @@ -3,5 +3,5 @@ export const SMALL = 'small'; export const MEDIUM = 'medium'; export const LARGE = 'large'; export const EXTRA_LARGE = 'extraLarge'; - -export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; +export const EXTRA_EXTRA_LARGE = 'extraExtraLarge'; +export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index e421db602..dbcd10613 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -36,6 +36,7 @@ import InteractiveImport, { import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; +import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal'; import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; import Language from 'Language/Language'; @@ -73,7 +74,8 @@ type SelectType = | 'releaseGroup' | 'quality' | 'language' - | 'indexerFlags'; + | 'indexerFlags' + | 'releaseType'; type FilterExistingFiles = 'all' | 'new'; @@ -128,6 +130,12 @@ const COLUMNS = [ isSortable: true, isVisible: true, }, + { + name: 'releaseType', + label: () => translate('ReleaseType'), + isSortable: true, + isVisible: true, + }, { name: 'customFormats', label: React.createElement(Icon, { @@ -369,6 +377,10 @@ function InteractiveImportModalContent( key: 'indexerFlags', value: translate('SelectIndexerFlags'), }, + { + key: 'releaseType', + value: translate('SelectReleaseType'), + }, ]; if (allowSeriesChange) { @@ -511,6 +523,7 @@ function InteractiveImportModalContent( languages, indexerFlags, episodeFileId, + releaseType, } = item; if (!series) { @@ -560,6 +573,7 @@ function InteractiveImportModalContent( quality, languages, indexerFlags, + releaseType, }); return; @@ -575,6 +589,7 @@ function InteractiveImportModalContent( quality, languages, indexerFlags, + releaseType, downloadId, episodeFileId, }); @@ -787,6 +802,22 @@ function InteractiveImportModalContent( [selectedIds, dispatch] ); + const onReleaseTypeSelect = useCallback( + (releaseType: string) => { + dispatch( + updateInteractiveImportItems({ + ids: selectedIds, + releaseType, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: selectedIds })); + + setSelectModalOpen(null); + }, + [selectedIds, dispatch] + ); + const orderedSelectedIds = items.reduce((acc: number[], file) => { if (selectedIds.includes(file.id)) { acc.push(file.id); @@ -1000,6 +1031,14 @@ function InteractiveImportModalContent( onModalClose={onSelectModalClose} /> + + { + setSelectModalOpen('releaseType'); + }, [setSelectModalOpen]); + + const onReleaseTypeSelect = useCallback( + (releaseType: ReleaseType) => { + dispatch( + updateInteractiveImportItem({ + id, + releaseType, + }) + ); + + dispatch(reprocessInteractiveImportItems({ ids: [id] })); + + setSelectModalOpen(null); + selectRowAfterChange(); + }, + [id, dispatch, setSelectModalOpen, selectRowAfterChange] + ); + const onSelectIndexerFlagsPress = useCallback(() => { setSelectModalOpen('indexerFlags'); }, [setSelectModalOpen]); @@ -461,6 +488,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { {formatBytes(size)} + + {getReleaseTypeName(releaseType)} + + {customFormats?.length ? ( + + diff --git a/frontend/src/InteractiveImport/ReleaseType.ts b/frontend/src/InteractiveImport/ReleaseType.ts new file mode 100644 index 000000000..7bfa8550d --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType.ts @@ -0,0 +1,3 @@ +type ReleaseType = 'unknown' | 'singleEpisode' | 'multiEpisode' | 'seasonPack'; + +export default ReleaseType; diff --git a/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx new file mode 100644 index 000000000..720914b43 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModal.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ReleaseType from 'InteractiveImport/ReleaseType'; +import SelectReleaseTypeModalContent from './SelectReleaseTypeModalContent'; + +interface SelectQualityModalProps { + isOpen: boolean; + releaseType: ReleaseType; + modalTitle: string; + onReleaseTypeSelect(releaseType: ReleaseType): void; + onModalClose(): void; +} + +function SelectReleaseTypeModal(props: SelectQualityModalProps) { + const { isOpen, releaseType, modalTitle, onReleaseTypeSelect, onModalClose } = + props; + + return ( + + + + ); +} + +export default SelectReleaseTypeModal; diff --git a/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx new file mode 100644 index 000000000..610811195 --- /dev/null +++ b/frontend/src/InteractiveImport/ReleaseType/SelectReleaseTypeModalContent.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useState } from 'react'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +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 { inputTypes, kinds } from 'Helpers/Props'; +import ReleaseType from 'InteractiveImport/ReleaseType'; +import translate from 'Utilities/String/translate'; + +const options = [ + { + key: 'unknown', + get value() { + return translate('Unknown'); + }, + }, + { + key: 'singleEpisode', + get value() { + return translate('SingleEpisode'); + }, + }, + { + key: 'multiEpisode', + get value() { + return translate('MultiEpisode'); + }, + }, + { + key: 'seasonPack', + get value() { + return translate('SeasonPack'); + }, + }, +]; + +interface SelectReleaseTypeModalContentProps { + releaseType: ReleaseType; + modalTitle: string; + onReleaseTypeSelect(releaseType: ReleaseType): void; + onModalClose(): void; +} + +function SelectReleaseTypeModalContent( + props: SelectReleaseTypeModalContentProps +) { + const { modalTitle, onReleaseTypeSelect, onModalClose } = props; + const [releaseType, setReleaseType] = useState(props.releaseType); + + const handleReleaseTypeChange = useCallback( + ({ value }: { value: string }) => { + setReleaseType(value as ReleaseType); + }, + [setReleaseType] + ); + + const handleReleaseTypeSelect = useCallback(() => { + onReleaseTypeSelect(releaseType); + }, [releaseType, onReleaseTypeSelect]); + + return ( + + + {modalTitle} - {translate('SelectReleaseType')} + + + +
+ + {translate('ReleaseType')} + + + +
+
+ + + + + + +
+ ); +} + +export default SelectReleaseTypeModalContent; diff --git a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx index 27b54f95b..434318334 100644 --- a/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx +++ b/frontend/src/Series/Index/Select/Edit/EditSeriesModalContent.tsx @@ -36,7 +36,7 @@ const monitoredOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'monitored', @@ -58,7 +58,7 @@ const seasonFolderOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'yes', diff --git a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js b/frontend/src/Series/Search/SeasonInteractiveSearchModal.js index a2210222a..861c9113c 100644 --- a/frontend/src/Series/Search/SeasonInteractiveSearchModal.js +++ b/frontend/src/Series/Search/SeasonInteractiveSearchModal.js @@ -15,7 +15,7 @@ function SeasonInteractiveSearchModal(props) { return ( diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js index 33497ce44..44fa9c5ca 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js @@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
+ +
+ {translate('CustomFormatsSettingsTriggerInfo')} +
+
{ specifications.map((tag) => { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js index f2509603f..34213928d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent'; import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import translate from 'Utilities/String/translate'; import styles from './EditDownloadClientModalContent.css'; @@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component { onModalClose, onSavePress, onTestPress, + onAdvancedSettingsPress, onDeleteDownloadClientPress, ...otherProps } = this.props; @@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component { } + + { + this.props.toggleAdvancedSettings(); + }; + // // Render @@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = { setDownloadClientFieldValue: PropTypes.func.isRequired, saveDownloadClient: PropTypes.func.isRequired, testDownloadClient: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx index 3a024b559..893e2542d 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/Edit/ManageDownloadClientsEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js deleted file mode 100644 index 57a7b0e2d..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.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 EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector'; - -function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) { - return ( - - - - ); -} - -EditImportListExclusionModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.tsx new file mode 100644 index 000000000..9b7afb3ba --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.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 EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; + +interface EditImportListExclusionModalProps { + id?: number; + isOpen: boolean; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function EditImportListExclusionModal( + props: EditImportListExclusionModalProps +) { + const { isOpen, onModalClose, ...otherProps } = props; + + const dispatch = useDispatch(); + + const onModalClosePress = useCallback(() => { + dispatch( + clearPendingChanges({ + section: 'settings.importListExclusions', + }) + ); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + + + ); +} + +export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js deleted file mode 100644 index cd4338621..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import EditImportListExclusionModal from './EditImportListExclusionModal'; - -function mapStateToProps() { - return {}; -} - -const mapDispatchToProps = { - clearPendingChanges -}; - -class EditImportListExclusionModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearPendingChanges({ section: 'settings.importListExclusions' }); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js deleted file mode 100644 index 284d1100c..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Alert from 'Components/Alert'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import Button from 'Components/Link/Button'; -import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -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 { inputTypes, kinds } from 'Helpers/Props'; -import { numberSettingShape, stringSettingShape } from 'Helpers/Props/Shapes/settingShape'; -import translate from 'Utilities/String/translate'; -import styles from './EditImportListExclusionModalContent.css'; - -function EditImportListExclusionModalContent(props) { - const { - id, - isFetching, - error, - isSaving, - saveError, - item, - onInputChange, - onSavePress, - onModalClose, - onDeleteImportListExclusionPress, - ...otherProps - } = props; - - const { - title, - tvdbId - } = item; - - return ( - - - {id ? translate('EditImportListExclusion') : translate('AddImportListExclusion')} - - - - { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('AddImportListExclusionError')} - - } - - { - !isFetching && !error && -
- - {translate('Title')} - - - - - - {translate('TvdbId')} - - - -
- } -
- - - { - id && - - } - - - - - {translate('Save')} - - -
- ); -} - -const ImportListExclusionShape = { - title: PropTypes.shape(stringSettingShape).isRequired, - tvdbId: PropTypes.shape(numberSettingShape).isRequired -}; - -EditImportListExclusionModalContent.propTypes = { - id: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.shape(ImportListExclusionShape).isRequired, - onInputChange: PropTypes.func.isRequired, - onSavePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired, - onDeleteImportListExclusionPress: PropTypes.func -}; - -export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx new file mode 100644 index 000000000..8570d1acf --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +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 { + saveImportListExclusion, + setImportListExclusionValue, +} from 'Store/Actions/settingsActions'; +import selectSettings from 'Store/Selectors/selectSettings'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import { PendingSection } from 'typings/pending'; +import translate from 'Utilities/String/translate'; +import styles from './EditImportListExclusionModalContent.css'; + +const newImportListExclusion = { + title: '', + tvdbId: 0, +}; + +interface EditImportListExclusionModalContentProps { + id?: number; + onModalClose: () => void; + onDeleteImportListExclusionPress?: () => void; +} + +function createImportListExclusionSelector(id?: number) { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + const { isFetching, error, isSaving, saveError, pendingChanges, items } = + importListExclusions; + + const mapping = id + ? items.find((i) => i.id === id) + : newImportListExclusion; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings as PendingSection, + ...settings, + }; + } + ); +} + +function EditImportListExclusionModalContent( + props: EditImportListExclusionModalContentProps +) { + const { id, onModalClose, onDeleteImportListExclusionPress } = props; + + const dispatch = useDispatch(); + + const dispatchSetImportListExclusionValue = (payload: { + name: string; + value: string | number; + }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }; + + const { isFetching, isSaving, item, error, saveError, ...otherProps } = + useSelector(createImportListExclusionSelector(props.id)); + const previousIsSaving = usePrevious(isSaving); + + const { title, tvdbId } = item; + + useEffect(() => { + if (!id) { + Object.keys(newImportListExclusion).forEach((name) => { + dispatchSetImportListExclusionValue({ + name, + value: + newImportListExclusion[name as keyof typeof newImportListExclusion], + }); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (previousIsSaving && !isSaving && !saveError) { + onModalClose(); + } + }); + + const onSavePress = useCallback(() => { + dispatch(saveImportListExclusion({ id })); + }, [dispatch, id]); + + const onInputChange = useCallback( + (payload: { name: string; value: string | number }) => { + // @ts-expect-error 'setImportListExclusionValue' isn't typed yet + dispatch(setImportListExclusionValue(payload)); + }, + [dispatch] + ); + + return ( + + + {id + ? translate('EditImportListExclusion') + : translate('AddImportListExclusion')} + + + + {isFetching && } + + {!isFetching && !!error && ( + + {translate('AddImportListExclusionError')} + + )} + + {!isFetching && !error && ( +
+ + {translate('Title')} + + + + + + {translate('TvdbId')} + + + +
+ )} +
+ + + {id && ( + + )} + + + + + {translate('Save')} + + +
+ ); +} + +export default EditImportListExclusionModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js deleted file mode 100644 index 059223231..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { saveImportListExclusion, setImportListExclusionValue } from 'Store/Actions/settingsActions'; -import selectSettings from 'Store/Selectors/selectSettings'; -import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; - -const newImportListExclusion = { - title: '', - tvdbId: 0 -}; - -function createImportListExclusionSelector() { - return createSelector( - (state, { id }) => id, - (state) => state.settings.importListExclusions, - (id, importListExclusions) => { - const { - isFetching, - error, - isSaving, - saveError, - pendingChanges, - items - } = importListExclusions; - - const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion; - const settings = selectSettings(mapping, pendingChanges, saveError); - - return { - id, - isFetching, - error, - isSaving, - saveError, - item: settings.settings, - ...settings - }; - } - ); -} - -function createMapStateToProps() { - return createSelector( - createImportListExclusionSelector(), - (importListExclusion) => { - return { - ...importListExclusion - }; - } - ); -} - -const mapDispatchToProps = { - setImportListExclusionValue, - saveImportListExclusion -}; - -class EditImportListExclusionModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - if (!this.props.id) { - Object.keys(newImportListExclusion).forEach((name) => { - this.props.setImportListExclusionValue({ - name, - value: newImportListExclusion[name] - }); - }); - } - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { - this.props.onModalClose(); - } - } - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setImportListExclusionValue({ name, value }); - }; - - onSavePress = () => { - this.props.saveImportListExclusion({ id: this.props.id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -EditImportListExclusionModalContentConnector.propTypes = { - id: PropTypes.number, - isSaving: PropTypes.bool.isRequired, - saveError: PropTypes.object, - item: PropTypes.object.isRequired, - setImportListExclusionValue: PropTypes.func.isRequired, - saveImportListExclusion: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css deleted file mode 100644 index 92e533c7e..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css +++ /dev/null @@ -1,25 +0,0 @@ -.importListExclusion { - display: flex; - align-items: stretch; - margin-bottom: 10px; - height: 30px; - border-bottom: 1px solid var(--borderColor); - line-height: 30px; -} - -.title { - @add-mixin truncate; - - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.actions { - display: flex; - justify-content: flex-end; - flex: 1 0 auto; - padding-right: 10px; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts index 213f9816d..d8ea83dc1 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css.d.ts @@ -2,9 +2,6 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'importListExclusion': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js deleted file mode 100644 index e95561b82..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js +++ /dev/null @@ -1,112 +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 ConfirmModal from 'Components/Modal/ConfirmModal'; -import { icons, kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import styles from './ImportListExclusion.css'; - -class ImportListExclusion extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onEditImportListExclusionPress = () => { - this.setState({ isEditImportListExclusionModalOpen: true }); - }; - - onEditImportListExclusionModalClose = () => { - this.setState({ isEditImportListExclusionModalOpen: false }); - }; - - onDeleteImportListExclusionPress = () => { - this.setState({ - isEditImportListExclusionModalOpen: false, - isDeleteImportListExclusionModalOpen: true - }); - }; - - onDeleteImportListExclusionModalClose = () => { - this.setState({ isDeleteImportListExclusionModalOpen: false }); - }; - - onConfirmDeleteImportListExclusion = () => { - this.props.onConfirmDeleteImportListExclusion(this.props.id); - }; - - // - // Render - - render() { - const { - id, - title, - tvdbId - } = this.props; - - return ( -
-
{title}
-
{tvdbId}
- -
- - - -
- - - - -
- ); - } -} - -ImportListExclusion.propTypes = { - id: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - tvdbId: PropTypes.number.isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -ImportListExclusion.defaultProps = { - // The drag preview will not connect the drag handle. - connectDragSource: (node) => node -}; - -export default ImportListExclusion; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css new file mode 100644 index 000000000..c154fa5a3 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css @@ -0,0 +1,6 @@ +.actions { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 35px; + white-space: nowrap; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts new file mode 100644 index 000000000..d8ea83dc1 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts @@ -0,0 +1,7 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'actions': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx new file mode 100644 index 000000000..37de7940a --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import ImportListExclusion from 'typings/ImportListExclusion'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import styles from './ImportListExclusionRow.css'; + +interface ImportListExclusionRowProps extends ImportListExclusion { + onConfirmDeleteImportListExclusion: (id: number) => void; +} + +function ImportListExclusionRow(props: ImportListExclusionRowProps) { + const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props; + + const [ + isEditImportListExclusionModalOpen, + setEditImportListExclusionModalOpen, + setEditImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const [ + isDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalOpen, + setDeleteImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const onConfirmDeleteImportListExclusionPress = useCallback(() => { + onConfirmDeleteImportListExclusion(id); + }, [id, onConfirmDeleteImportListExclusion]); + + return ( + + {title} + {tvdbId} + + + + + + + + + + ); +} + +export default ImportListExclusionRow; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css deleted file mode 100644 index ecb080585..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css +++ /dev/null @@ -1,23 +0,0 @@ -.importListExclusionsHeader { - display: flex; - margin-bottom: 10px; - font-weight: bold; -} - -.title { - flex: 0 1 600px; -} - -.tvdbId { - flex: 0 0 70px; -} - -.addImportListExclusion { - display: flex; - justify-content: flex-end; - padding-right: 10px; -} - -.addButton { - text-align: center; -} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts index 6cb93f7ce..626717e71 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css.d.ts @@ -3,9 +3,6 @@ interface CssExports { 'addButton': string; 'addImportListExclusion': string; - 'importListExclusionsHeader': string; - 'title': string; - 'tvdbId': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js deleted file mode 100644 index 9bb7814d9..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js +++ /dev/null @@ -1,105 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import PageSectionContent from 'Components/Page/PageSectionContent'; -import { icons } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector'; -import ImportListExclusion from './ImportListExclusion'; -import styles from './ImportListExclusions.css'; - -class ImportListExclusions extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isAddImportListExclusionModalOpen: false - }; - } - - // - // Listeners - - onAddImportListExclusionPress = () => { - this.setState({ isAddImportListExclusionModalOpen: true }); - }; - - onModalClose = () => { - this.setState({ isAddImportListExclusionModalOpen: false }); - }; - - // - // Render - - render() { - const { - items, - onConfirmDeleteImportListExclusion, - ...otherProps - } = this.props; - - return ( -
- -
-
- {translate('Title')} -
-
- {translate('TvdbId')} -
-
- -
- { - items.map((item, index) => { - return ( - - ); - }) - } -
- -
- - - -
- - - -
-
- ); - } -} - -ImportListExclusions.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onConfirmDeleteImportListExclusion: PropTypes.func.isRequired -}; - -export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx new file mode 100644 index 000000000..8c7033686 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -0,0 +1,232 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import IconButton from 'Components/Link/IconButton'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons } from 'Helpers/Props'; +import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions'; +import { + registerPagePopulator, + unregisterPagePopulator, +} from 'Utilities/pagePopulator'; +import translate from 'Utilities/String/translate'; +import EditImportListExclusionModal from './EditImportListExclusionModal'; +import ImportListExclusionRow from './ImportListExclusionRow'; + +const COLUMNS = [ + { + name: 'title', + label: () => translate('Title'), + isVisible: true, + isSortable: true, + }, + { + name: 'tvdbid', + label: () => translate('TvdbId'), + isVisible: true, + isSortable: true, + }, + { + name: 'actions', + isVisible: true, + isSortable: false, + }, +]; + +function createImportListExlucionsSelector() { + return createSelector( + (state: AppState) => state.settings.importListExclusions, + (importListExclusions) => { + return { + ...importListExclusions, + }; + } + ); +} + +function ImportListExclusions() { + const history = useHistory(); + const useCurrentPage = history.action === 'POP'; + + const dispatch = useDispatch(); + + const fetchImportListExclusions = useCallback(() => { + dispatch(importListExclusionActions.fetchImportListExclusions()); + }, [dispatch]); + + const deleteImportListExclusion = useCallback( + (payload: { id: number }) => { + dispatch(importListExclusionActions.deleteImportListExclusion(payload)); + }, + [dispatch] + ); + + const gotoImportListExclusionFirstPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + }, [dispatch]); + + const gotoImportListExclusionPreviousPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage()); + }, [dispatch]); + + const gotoImportListExclusionNextPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionNextPage()); + }, [dispatch]); + + const gotoImportListExclusionLastPage = useCallback(() => { + dispatch(importListExclusionActions.gotoImportListExclusionLastPage()); + }, [dispatch]); + + const gotoImportListExclusionPage = useCallback( + (page: number) => { + dispatch( + importListExclusionActions.gotoImportListExclusionPage({ page }) + ); + }, + [dispatch] + ); + + const setImportListExclusionSort = useCallback( + (sortKey: { sortKey: string }) => { + dispatch( + importListExclusionActions.setImportListExclusionSort({ sortKey }) + ); + }, + [dispatch] + ); + + const setImportListTableOption = useCallback( + (payload: { pageSize: number }) => { + dispatch( + importListExclusionActions.setImportListExclusionTableOption(payload) + ); + + if (payload.pageSize) { + dispatch(importListExclusionActions.gotoImportListExclusionFirstPage()); + } + }, + [dispatch] + ); + + const repopulate = useCallback(() => { + gotoImportListExclusionFirstPage(); + }, [gotoImportListExclusionFirstPage]); + + useEffect(() => { + registerPagePopulator(repopulate); + + if (useCurrentPage) { + fetchImportListExclusions(); + } else { + gotoImportListExclusionFirstPage(); + } + + return () => unregisterPagePopulator(repopulate); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onConfirmDeleteImportListExclusion = useCallback( + (id: number) => { + deleteImportListExclusion({ id }); + repopulate(); + }, + [deleteImportListExclusion, repopulate] + ); + + const selected = useSelector(createImportListExlucionsSelector()); + + const { + isFetching, + isPopulated, + items, + pageSize, + sortKey, + error, + sortDirection, + totalRecords, + ...otherProps + } = selected; + + const [ + isAddImportListExclusionModalOpen, + setAddImportListExclusionModalOpen, + setAddImportListExclusionModalClosed, + ] = useModalOpenState(false); + + const isFetchingForFirstTime = isFetching && !isPopulated; + + return ( +
+ + + + {items.map((item) => { + return ( + + ); + })} + + + + + + + + + + +
+ + + + +
+
+ ); +} + +export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js deleted file mode 100644 index 184788cec..000000000 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js +++ /dev/null @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { deleteImportListExclusion, fetchImportListExclusions } from 'Store/Actions/settingsActions'; -import ImportListExclusions from './ImportListExclusions'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.importListExclusions, - (importListExclusions) => { - return { - ...importListExclusions - }; - } - ); -} - -const mapDispatchToProps = { - fetchImportListExclusions, - deleteImportListExclusion -}; - -class ImportListExclusionsConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.fetchImportListExclusions(); - } - - // - // Listeners - - onConfirmDeleteImportListExclusion = (id) => { - this.props.deleteImportListExclusion({ id }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -ImportListExclusionsConnector.propTypes = { - fetchImportListExclusions: PropTypes.func.isRequired, - deleteImportListExclusion: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index de1d486b6..1ec50526e 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector'; +import ImportListsExclusions from './ImportListExclusions/ImportListExclusions'; import ImportListsConnector from './ImportLists/ImportListsConnector'; import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal'; import ImportListOptions from './Options/ImportListOptions'; @@ -113,7 +113,7 @@ class ImportListSettings extends Component { onChildStateChange={this.onChildStateChange} /> - + } + + { + this.props.toggleAdvancedSettings(); + }; + // // Render @@ -65,6 +76,7 @@ class EditImportListModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -82,6 +94,7 @@ EditImportListModalContentConnector.propTypes = { setImportListFieldValue: PropTypes.func.isRequired, saveImportList: PropTypes.func.isRequired, testImportList: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx index 8660f2fd3..f95d65314 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/Edit/ManageImportListsEditModalContent.tsx @@ -31,7 +31,7 @@ const autoAddOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx index a7b7187e3..00555433c 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/Edit/ManageIndexersEditModalContent.tsx @@ -32,7 +32,7 @@ const enableOptions = [ get value() { return translate('NoChange'); }, - disabled: true, + isDisabled: true, }, { key: 'enabled', diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 509c5d940..eec2449cd 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -80,19 +80,19 @@ const fileNameTokens = [ ]; const seriesTokens = [ - { token: '{Series Title}', example: 'The Series Title\'s!' }, - { token: '{Series CleanTitle}', example: 'The Series Title\'s!' }, - { token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' }, - { token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' }, - { token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' }, - { token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' }, - { token: '{Series TitleThe}', example: 'Series Title\'s!, The' }, - { token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' }, - { token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' }, - { token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' }, - { token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' }, - { token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' }, - { token: '{Series TitleFirstCharacter}', example: 'S' }, + { token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 }, + { token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 }, + { token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 }, + { token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 }, + { token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 }, + { token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 }, + { token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 }, + { token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 }, + { token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 }, + { token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 }, + { token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 }, + { token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 }, + { token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 }, { token: '{Series Year}', example: '2010' } ]; @@ -124,8 +124,8 @@ const absoluteTokens = [ ]; const episodeTitleTokens = [ - { token: '{Episode Title}', example: 'Episode\'s Title' }, - { token: '{Episode CleanTitle}', example: 'Episodes Title' } + { token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 }, + { token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 } ]; const qualityTokens = [ @@ -149,8 +149,13 @@ const mediaInfoTokens = [ ]; const otherTokens = [ - { token: '{Release Group}', example: 'Rls Grp' }, - { token: '{Custom Formats}', example: 'iNTERNAL' } + { token: '{Release Group}', example: 'Rls Grp', footNote: 1 }, + { token: '{Custom Formats}', example: 'iNTERNAL' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' } +]; + +const otherAnimeTokens = [ + { token: '{Release Hash}', example: 'ABCDEFGH' } ]; const originalTokens = [ @@ -300,7 +305,7 @@ class NamingModal extends Component {
{ - seriesTokens.map(({ token, example }) => { + seriesTokens.map(({ token, example, footNote }) => { return ( + +
+ + +
@@ -446,7 +457,7 @@ class NamingModal extends Component {
{ - episodeTitleTokens.map(({ token, example }) => { + episodeTitleTokens.map(({ token, example, footNote }) => { return ( +
+ + +
@@ -518,7 +534,26 @@ class NamingModal extends Component {
{ - otherTokens.map(({ token, example }) => { + otherTokens.map(({ token, example, footNote }) => { + return ( + + ); + } + ) + } + + { + anime && otherAnimeTokens.map(({ token, example }) => { return ( + +
+ + +
diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index a09c91ec8..204c93d0e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -26,7 +26,7 @@ .token { flex: 0 0 50%; - padding: 6px 6px; + padding: 6px; background-color: var(--popoverTitleBackgroundColor); font-family: $monoSpaceFontFamily; } @@ -36,7 +36,7 @@ align-items: center; justify-content: space-between; flex: 0 0 50%; - padding: 6px 6px; + padding: 6px; background-color: var(--popoverBodyBackgroundColor); .footNote { diff --git a/frontend/src/Settings/MetadataSource/TheTvdb.js b/frontend/src/Settings/MetadataSource/TheTvdb.js index b1abb0c99..79a1e6e40 100644 --- a/frontend/src/Settings/MetadataSource/TheTvdb.js +++ b/frontend/src/Settings/MetadataSource/TheTvdb.js @@ -4,7 +4,6 @@ import translate from 'Utilities/String/translate'; import styles from './TheTvdb.css'; function TheTvdb(props) { - debugger; return (
} + + { + this.props.toggleAdvancedSettings(); + }; + // // Render @@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component { {...this.props} onSavePress={this.onSavePress} onTestPress={this.onTestPress} + onAdvancedSettingsPress={this.onAdvancedSettingsPress} onInputChange={this.onInputChange} onFieldChange={this.onFieldChange} /> @@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = { setNotificationFieldValue: PropTypes.func.isRequired, saveNotification: PropTypes.func.isRequired, testNotification: PropTypes.func.isRequired, + toggleAdvancedSettings: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Settings/Tags/TagInUse.js b/frontend/src/Settings/Tags/TagInUse.js index 9fb57d230..27228fa2e 100644 --- a/frontend/src/Settings/Tags/TagInUse.js +++ b/frontend/src/Settings/Tags/TagInUse.js @@ -12,7 +12,7 @@ export default function TagInUse(props) { return null; } - if (count > 1 && labelPlural ) { + if (count > 1 && labelPlural) { return (
{count} {labelPlural.toLowerCase()} diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index b9b38a0ef..d6371946f 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -1,9 +1,11 @@ import { createAction } from 'redux-actions'; -import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; +import createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; -import { createThunk } from 'Store/thunks'; +import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; +import { createThunk, handleThunks } from 'Store/thunks'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; // // Variables @@ -14,6 +16,13 @@ const section = 'settings.importListExclusions'; // Actions Types export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions'; +export const GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionFirstPage'; +export const GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPreviousPage'; +export const GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionNextPage'; +export const GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionLastPage'; +export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage'; +export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort'; +export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption'; export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion'; export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion'; export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue'; @@ -22,9 +31,16 @@ export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/se // Action Creators export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS); +export const gotoImportListExclusionFirstPage = createThunk(GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE); +export const gotoImportListExclusionPreviousPage = createThunk(GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE); +export const gotoImportListExclusionNextPage = createThunk(GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE); +export const gotoImportListExclusionLastPage = createThunk(GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE); +export const gotoImportListExclusionPage = createThunk(GOTO_IMPORT_LIST_EXCLUSION_PAGE); +export const setImportListExclusionSort = createThunk(SET_IMPORT_LIST_EXCLUSION_SORT); export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION); export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION); +export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION); export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => { return { section, @@ -44,6 +60,7 @@ export default { isFetching: false, isPopulated: false, error: null, + pageSize: 20, items: [], isSaving: false, saveError: null, @@ -53,17 +70,31 @@ export default { // // Action Handlers - actionHandlers: { - [FETCH_IMPORT_LIST_EXCLUSIONS]: createFetchHandler(section, '/importlistexclusion'), + actionHandlers: handleThunks({ + ...createServerSideCollectionHandlers( + section, + '/importlistexclusion/paged', + fetchImportListExclusions, + { + [serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS, + [serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_IMPORT_LIST_EXCLUSION_PAGE, + [serverSideCollectionHandlers.SORT]: SET_IMPORT_LIST_EXCLUSION_SORT + } + ), [SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'), [DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion') - }, + }), // // Reducers reducers: { - [SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section) + [SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section), + [SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section) } }; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index ce6da8a21..ed05ed548 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -163,6 +163,7 @@ export const actionHandlers = handleThunks({ languages: item.languages, releaseGroup: item.releaseGroup, indexerFlags: item.indexerFlags, + releaseType: item.releaseType, downloadId: item.downloadId }; }); diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index b14bc19e4..6d7495321 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -250,6 +250,12 @@ export const defaultState = { label: () => translate('SeasonPack'), type: filterBuilderTypes.EXACT, valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'episodeRequested', + label: () => translate('EpisodeRequested'), + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL } ], diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index e7b5e40f6..440f20000 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -1,5 +1,4 @@ import { createAction } from 'redux-actions'; -import indexerFlags from 'Store/Actions/Settings/indexerFlags'; import { handleThunks } from 'Store/thunks'; import createHandleActions from './Creators/createHandleActions'; import autoTaggings from './Settings/autoTaggings'; @@ -13,6 +12,7 @@ import general from './Settings/general'; import importListExclusions from './Settings/importListExclusions'; import importListOptions from './Settings/importListOptions'; import importLists from './Settings/importLists'; +import indexerFlags from './Settings/indexerFlags'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; @@ -91,7 +91,8 @@ export const defaultState = { }; export const persistState = [ - 'settings.advancedSettings' + 'settings.advancedSettings', + 'settings.importListExclusions.pageSize' ]; // diff --git a/frontend/src/Store/Selectors/createMultiSeriesSelector.ts b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts new file mode 100644 index 000000000..fa8235c45 --- /dev/null +++ b/frontend/src/Store/Selectors/createMultiSeriesSelector.ts @@ -0,0 +1,23 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Series from 'Series/Series'; + +function createMultiSeriesSelector(seriesIds: number[]) { + return createSelector( + (state: AppState) => state.series.itemMap, + (state: AppState) => state.series.items, + (itemMap, allSeries) => { + return seriesIds.reduce((acc: Series[], seriesId) => { + const series = allSeries[itemMap[seriesId]]; + + if (series) { + acc.push(series); + } + + return acc; + }, []); + } + ); +} + +export default createMultiSeriesSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts index f43e4e59b..ad1e9cd6b 100644 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.ts +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.ts @@ -1,45 +1,44 @@ import { createSelector } from 'reselect'; -import AppSectionState, { - AppSectionItemState, -} from 'App/State/AppSectionState'; +import { AppSectionItemState } from 'App/State/AppSectionState'; import AppState from 'App/State/AppState'; +import SettingsAppState from 'App/State/SettingsAppState'; import selectSettings from 'Store/Selectors/selectSettings'; import { PendingSection } from 'typings/pending'; -type SettingNames = keyof Omit; -type GetSectionState = AppState['settings'][Name]; -type GetSettingsSectionItemType = - GetSectionState extends AppSectionItemState - ? R - : GetSectionState extends AppSectionState - ? R +type SectionsWithItemNames = { + [K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState + ? K : never; +}[keyof SettingsAppState]; -type AppStateWithPending = { - item?: GetSettingsSectionItemType; - pendingChanges?: Partial>; - saveError?: Error; -} & GetSectionState; +type GetSectionState = + SettingsAppState[Name]; +type GetSettingsSectionItemType = + GetSectionState extends AppSectionItemState ? R : never; -function createSettingsSectionSelector( - section: Name -) { +function createSettingsSectionSelector< + Name extends SectionsWithItemNames, + T extends GetSettingsSectionItemType +>(section: Name) { return createSelector( (state: AppState) => state.settings[section], (sectionSettings) => { - const { item, pendingChanges, saveError, ...other } = - sectionSettings as AppStateWithPending; + const { item, pendingChanges, ...other } = sectionSettings; - const { settings, ...rest } = selectSettings( - item, - pendingChanges, - saveError - ); + const saveError = + 'saveError' in sectionSettings ? sectionSettings.saveError : undefined; + + const { + settings, + pendingChanges: selectedPendingChanges, + ...rest + } = selectSettings(item, pendingChanges, saveError); return { ...other, saveError, - settings: settings as PendingSection>, + settings: settings as PendingSection, + pendingChanges: selectedPendingChanges as Partial, ...rest, }; } diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css index 034804711..6e38929c9 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css @@ -10,15 +10,6 @@ width: 100%; } -.commandName { - display: inline-block; - min-width: 220px; -} - -.userAgent { - color: #b0b0b0; -} - .queued, .started, .ended { diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts index 3bc00b738..2c6010533 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts @@ -2,14 +2,12 @@ // Please do not change this file! interface CssExports { 'actions': string; - 'commandName': string; 'duration': string; 'ended': string; 'queued': string; 'started': string; 'trigger': string; 'triggerContent': string; - 'userAgent': string; } export const cssExports: CssExports; export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js deleted file mode 100644 index 8b8a62d3a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js +++ /dev/null @@ -1,279 +0,0 @@ -import moment from 'moment'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import IconButton from 'Components/Link/IconButton'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableRow from 'Components/Table/TableRow'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; -import titleCase from 'Utilities/String/titleCase'; -import translate from 'Utilities/String/translate'; -import styles from './QueuedTaskRow.css'; - -function getStatusIconProps(status, message) { - const title = titleCase(status); - - switch (status) { - case 'queued': - return { - name: icons.PENDING, - title - }; - - case 'started': - return { - name: icons.REFRESH, - isSpinning: true, - title - }; - - case 'completed': - return { - name: icons.CHECK, - kind: kinds.SUCCESS, - title: message === 'Completed' ? title : `${title}: ${message}` - }; - - case 'failed': - return { - name: icons.FATAL, - kind: kinds.DANGER, - title: `${title}: ${message}` - }; - - default: - return { - name: icons.UNKNOWN, - title - }; - } -} - -function getFormattedDates(props) { - const { - queued, - started, - ended, - showRelativeDates, - shortDateFormat - } = props; - - if (showRelativeDates) { - return { - queuedAt: moment(queued).fromNow(), - startedAt: started ? moment(started).fromNow() : '-', - endedAt: ended ? moment(ended).fromNow() : '-' - }; - } - - return { - queuedAt: formatDate(queued, shortDateFormat), - startedAt: started ? formatDate(started, shortDateFormat) : '-', - endedAt: ended ? formatDate(ended, shortDateFormat) : '-' - }; -} - -class QueuedTaskRow extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - ...getFormattedDates(props), - isCancelConfirmModalOpen: false - }; - - this._updateTimeoutId = null; - } - - componentDidMount() { - this.setUpdateTimer(); - } - - componentDidUpdate(prevProps) { - const { - queued, - started, - ended - } = this.props; - - if ( - queued !== prevProps.queued || - started !== prevProps.started || - ended !== prevProps.ended - ) { - this.setState(getFormattedDates(this.props)); - } - } - - componentWillUnmount() { - if (this._updateTimeoutId) { - this._updateTimeoutId = clearTimeout(this._updateTimeoutId); - } - } - - // - // Control - - setUpdateTimer() { - this._updateTimeoutId = setTimeout(() => { - this.setState(getFormattedDates(this.props)); - this.setUpdateTimer(); - }, 30000); - } - - // - // Listeners - - onCancelPress = () => { - this.setState({ - isCancelConfirmModalOpen: true - }); - }; - - onAbortCancel = () => { - this.setState({ - isCancelConfirmModalOpen: false - }); - }; - - // - // Render - - render() { - const { - trigger, - commandName, - queued, - started, - ended, - status, - duration, - message, - clientUserAgent, - longDateFormat, - timeFormat, - onCancelPress - } = this.props; - - const { - queuedAt, - startedAt, - endedAt, - isCancelConfirmModalOpen - } = this.state; - - let triggerIcon = icons.QUICK; - - if (trigger === 'manual') { - triggerIcon = icons.INTERACTIVE; - } else if (trigger === 'scheduled') { - triggerIcon = icons.SCHEDULED; - } - - return ( - - - - - - - - - - - - {commandName} - - { - clientUserAgent ? - - {translate('From')}: {clientUserAgent} - : - null - } - - - - {queuedAt} - - - - {startedAt} - - - - {endedAt} - - - - {formatTimeSpan(duration)} - - - - { - status === 'queued' && - - } - - - - - ); - } -} - -QueuedTaskRow.propTypes = { - trigger: PropTypes.string.isRequired, - commandName: PropTypes.string.isRequired, - queued: PropTypes.string.isRequired, - started: PropTypes.string, - ended: PropTypes.string, - status: PropTypes.string.isRequired, - duration: PropTypes.string, - message: PropTypes.string, - clientUserAgent: PropTypes.string, - showRelativeDates: PropTypes.bool.isRequired, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onCancelPress: PropTypes.func.isRequired -}; - -export default QueuedTaskRow; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx new file mode 100644 index 000000000..4511bcbf4 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx @@ -0,0 +1,238 @@ +import moment from 'moment'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { CommandBody } from 'Commands/Command'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRow from 'Components/Table/TableRow'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { icons, kinds } from 'Helpers/Props'; +import { cancelCommand } from 'Store/Actions/commandActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import titleCase from 'Utilities/String/titleCase'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRowNameCell from './QueuedTaskRowNameCell'; +import styles from './QueuedTaskRow.css'; + +function getStatusIconProps(status: string, message: string | undefined) { + const title = titleCase(status); + + switch (status) { + case 'queued': + return { + name: icons.PENDING, + title, + }; + + case 'started': + return { + name: icons.REFRESH, + isSpinning: true, + title, + }; + + case 'completed': + return { + name: icons.CHECK, + kind: kinds.SUCCESS, + title: message === 'Completed' ? title : `${title}: ${message}`, + }; + + case 'failed': + return { + name: icons.FATAL, + kind: kinds.DANGER, + title: `${title}: ${message}`, + }; + + default: + return { + name: icons.UNKNOWN, + title, + }; + } +} + +function getFormattedDates( + queued: string, + started: string | undefined, + ended: string | undefined, + showRelativeDates: boolean, + shortDateFormat: string +) { + if (showRelativeDates) { + return { + queuedAt: moment(queued).fromNow(), + startedAt: started ? moment(started).fromNow() : '-', + endedAt: ended ? moment(ended).fromNow() : '-', + }; + } + + return { + queuedAt: formatDate(queued, shortDateFormat), + startedAt: started ? formatDate(started, shortDateFormat) : '-', + endedAt: ended ? formatDate(ended, shortDateFormat) : '-', + }; +} + +interface QueuedTimes { + queuedAt: string; + startedAt: string; + endedAt: string; +} + +export interface QueuedTaskRowProps { + id: number; + trigger: string; + commandName: string; + queued: string; + started?: string; + ended?: string; + status: string; + duration?: string; + message?: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRow(props: QueuedTaskRowProps) { + const { + id, + trigger, + commandName, + queued, + started, + ended, + status, + duration, + message, + body, + clientUserAgent, + } = props; + + const dispatch = useDispatch(); + const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } = + useSelector(createUISettingsSelector()); + + const updateTimeTimeoutId = useRef | null>( + null + ); + const [times, setTimes] = useState( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + + const [ + isCancelConfirmModalOpen, + openCancelConfirmModal, + closeCancelConfirmModal, + ] = useModalOpenState(false); + + const handleCancelPress = useCallback(() => { + dispatch(cancelCommand({ id })); + }, [id, dispatch]); + + useEffect(() => { + updateTimeTimeoutId.current = setTimeout(() => { + setTimes( + getFormattedDates( + queued, + started, + ended, + showRelativeDates, + shortDateFormat + ) + ); + }, 30000); + + return () => { + if (updateTimeTimeoutId.current) { + clearTimeout(updateTimeTimeoutId.current); + } + }; + }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]); + + const { queuedAt, startedAt, endedAt } = times; + + let triggerIcon = icons.QUICK; + + if (trigger === 'manual') { + triggerIcon = icons.INTERACTIVE; + } else if (trigger === 'scheduled') { + triggerIcon = icons.SCHEDULED; + } + + return ( + + + + + + + + + + + + + {queuedAt} + + + + {startedAt} + + + + {endedAt} + + + + {formatTimeSpan(duration)} + + + + {status === 'queued' && ( + + )} + + + + + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js deleted file mode 100644 index f55ab985a..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js +++ /dev/null @@ -1,31 +0,0 @@ -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { cancelCommand } from 'Store/Actions/commandActions'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import QueuedTaskRow from './QueuedTaskRow'; - -function createMapStateToProps() { - return createSelector( - createUISettingsSelector(), - (uiSettings) => { - return { - showRelativeDates: uiSettings.showRelativeDates, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -function createMapDispatchToProps(dispatch, props) { - return { - onCancelPress() { - dispatch(cancelCommand({ - id: props.id - })); - } - }; -} - -export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css new file mode 100644 index 000000000..41acb33f8 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css @@ -0,0 +1,8 @@ +.commandName { + display: inline-block; + min-width: 220px; +} + +.userAgent { + color: #b0b0b0; +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts new file mode 100644 index 000000000..fc9081492 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'commandName': string; + 'userAgent': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx new file mode 100644 index 000000000..a3e327e01 --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { CommandBody } from 'Commands/Command'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector'; +import translate from 'Utilities/String/translate'; +import styles from './QueuedTaskRowNameCell.css'; + +export interface QueuedTaskRowNameCellProps { + commandName: string; + body: CommandBody; + clientUserAgent?: string; +} + +export default function QueuedTaskRowNameCell( + props: QueuedTaskRowNameCellProps +) { + const { commandName, body, clientUserAgent } = props; + const seriesIds = [...(body.seriesIds ?? [])]; + + if (body.seriesId) { + seriesIds.push(body.seriesId); + } + + const series = useSelector(createMultiSeriesSelector(seriesIds)); + const sortedSeries = series.sort((a, b) => + a.sortTitle.localeCompare(b.sortTitle) + ); + + return ( + + + {commandName} + {sortedSeries.length ? ( + - {sortedSeries.map((s) => s.title).join(', ')} + ) : null} + {body.seasonNumber ? ( + + {' '} + {translate('SeasonNumberToken', { + seasonNumber: body.seasonNumber, + })} + + ) : null} + + + {clientUserAgent ? ( + + {translate('From')}: {clientUserAgent} + + ) : null} + + ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js deleted file mode 100644 index dac38f1d4..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasks.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import FieldSet from 'Components/FieldSet'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import translate from 'Utilities/String/translate'; -import QueuedTaskRowConnector from './QueuedTaskRowConnector'; - -const columns = [ - { - name: 'trigger', - label: '', - isVisible: true - }, - { - name: 'commandName', - label: () => translate('Name'), - isVisible: true - }, - { - name: 'queued', - label: () => translate('Queued'), - isVisible: true - }, - { - name: 'started', - label: () => translate('Started'), - isVisible: true - }, - { - name: 'ended', - label: () => translate('Ended'), - isVisible: true - }, - { - name: 'duration', - label: () => translate('Duration'), - isVisible: true - }, - { - name: 'actions', - isVisible: true - } -]; - -function QueuedTasks(props) { - const { - isFetching, - isPopulated, - items - } = props; - - return ( -
- { - isFetching && !isPopulated && - - } - - { - isPopulated && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- ); -} - -QueuedTasks.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired -}; - -export default QueuedTasks; diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx new file mode 100644 index 000000000..e79deed7c --- /dev/null +++ b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx @@ -0,0 +1,74 @@ +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import { fetchCommands } from 'Store/Actions/commandActions'; +import translate from 'Utilities/String/translate'; +import QueuedTaskRow from './QueuedTaskRow'; + +const columns = [ + { + name: 'trigger', + label: '', + isVisible: true, + }, + { + name: 'commandName', + label: () => translate('Name'), + isVisible: true, + }, + { + name: 'queued', + label: () => translate('Queued'), + isVisible: true, + }, + { + name: 'started', + label: () => translate('Started'), + isVisible: true, + }, + { + name: 'ended', + label: () => translate('Ended'), + isVisible: true, + }, + { + name: 'duration', + label: () => translate('Duration'), + isVisible: true, + }, + { + name: 'actions', + isVisible: true, + }, +]; + +export default function QueuedTasks() { + const dispatch = useDispatch(); + const { isFetching, isPopulated, items } = useSelector( + (state: AppState) => state.commands + ); + + useEffect(() => { + dispatch(fetchCommands()); + }, [dispatch]); + + return ( +
+ {isFetching && !isPopulated && } + + {isPopulated && ( + + + {items.map((item) => { + return ; + })} + +
+ )} +
+ ); +} diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js deleted file mode 100644 index 5fa4d9ead..000000000 --- a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js +++ /dev/null @@ -1,46 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { fetchCommands } from 'Store/Actions/commandActions'; -import QueuedTasks from './QueuedTasks'; - -function createMapStateToProps() { - return createSelector( - (state) => state.commands, - (commands) => { - return commands; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchCommands: fetchCommands -}; - -class QueuedTasksConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchCommands(); - } - - // - // Render - - render() { - return ( - - ); - } -} - -QueuedTasksConnector.propTypes = { - dispatchFetchCommands: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js index 032dbede8..03a3b6ce4 100644 --- a/frontend/src/System/Tasks/Tasks.js +++ b/frontend/src/System/Tasks/Tasks.js @@ -2,7 +2,7 @@ import React from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import translate from 'Utilities/String/translate'; -import QueuedTasksConnector from './Queued/QueuedTasksConnector'; +import QueuedTasks from './Queued/QueuedTasks'; import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector'; function Tasks() { @@ -10,7 +10,7 @@ function Tasks() { - + ); diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs index 97a0104ee..3f5ec6f2a 100644 --- a/frontend/src/index.ejs +++ b/frontend/src/index.ejs @@ -3,13 +3,16 @@ - - + + + + + diff --git a/frontend/src/login.html b/frontend/src/login.html index 4c16da6be..e89099276 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -3,13 +3,16 @@ - - + + + + + diff --git a/frontend/src/typings/ImportListExclusion.ts b/frontend/src/typings/ImportListExclusion.ts new file mode 100644 index 000000000..ec9add4dd --- /dev/null +++ b/frontend/src/typings/ImportListExclusion.ts @@ -0,0 +1,6 @@ +import ModelBase from 'App/ModelBase'; + +export default interface ImportListExclusion extends ModelBase { + tvdbId: number; + title: string; +} diff --git a/frontend/src/typings/pending.ts b/frontend/src/typings/pending.ts index 53e885bcb..5cdcbc003 100644 --- a/frontend/src/typings/pending.ts +++ b/frontend/src/typings/pending.ts @@ -1,7 +1,21 @@ +export interface ValidationFailure { + propertyName: string; + errorMessage: string; + severity: 'error' | 'warning'; +} + +export interface ValidationError extends ValidationFailure { + isWarning: false; +} + +export interface ValidationWarning extends ValidationFailure { + isWarning: true; +} + export interface Pending { value: T; - errors: any[]; - warnings: any[]; + errors: ValidationError[]; + warnings: ValidationWarning[]; } export type PendingSection = { diff --git a/global.json b/global.json index 5f5ece165..e72335a83 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "6.0.405" + "version": "8.0.201" } } diff --git a/package.json b/package.json index 267e78f5f..d29ffb68c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "watch": "webpack --watch --config ./frontend/build/webpack.config.js", "lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/", "lint-fix": "yarn lint --fix", - "stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc" + "stylelint": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc" }, "repository": "https://github.com/Sonarr/Sonarr", "author": "Team Sonarr", @@ -27,7 +27,7 @@ "@fortawesome/free-solid-svg-icons": "6.4.0", "@fortawesome/react-fontawesome": "0.2.0", "@juggle/resize-observer": "3.4.0", - "@microsoft/signalr": "6.0.21", + "@microsoft/signalr": "8.0.0", "@sentry/browser": "7.100.0", "@types/node": "18.16.8", "@types/react": "18.2.6", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ef0944ea9..6e6430173 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0-all + 8.0-all true true false @@ -98,13 +98,6 @@ $(MSBuildProjectName.Replace('Sonarr','NzbDrone')) - - - - - - - @@ -129,14 +122,14 @@ - - - - + + + + - - + + @@ -175,16 +168,46 @@ + + + + + x64 + + + + + x86 + + + + + arm64 + + + + + arm + + + + + + + + + <_UsingDefaultRuntimeIdentifier>true - win-x64 + win-$(Architecture) <_UsingDefaultRuntimeIdentifier>true - linux-x64 + linux-$(Architecture) - net6.0 + net8.0 diff --git a/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj b/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj index 9cf833789..73483a610 100644 --- a/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj +++ b/src/NzbDrone.Automation.Test/Sonarr.Automation.Test.csproj @@ -1,11 +1,11 @@  - net6.0 + net8.0 - - + + diff --git a/src/NzbDrone.Automation.Test/app.config b/src/NzbDrone.Automation.Test/app.config index d660afa84..8da9b15e4 100644 --- a/src/NzbDrone.Automation.Test/app.config +++ b/src/NzbDrone.Automation.Test/app.config @@ -3,6 +3,6 @@ - + diff --git a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs index 6b1ea4171..cd8ed3476 100644 --- a/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs +++ b/src/NzbDrone.Common.Test/InstrumentationTests/CleanseLogMessageFixture.cs @@ -18,6 +18,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests [TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")] [TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")] [TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")] + [TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")] + [TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")] // NzbGet [TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")] diff --git a/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj b/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj index 402cae2bf..1a553cf8e 100644 --- a/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/Sonarr.Common.Test.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 diff --git a/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs index ffdc36e0e..d96bb22e9 100644 --- a/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs +++ b/src/NzbDrone.Common/Disk/DestinationAlreadyExistsException.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.Serialization; namespace NzbDrone.Common.Disk { @@ -24,10 +23,5 @@ namespace NzbDrone.Common.Disk : base(message, innerException) { } - - protected DestinationAlreadyExistsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index daa2cc449..f0231cb2c 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -21,7 +21,7 @@ namespace NzbDrone.Common.EnvironmentInfo IsWindowsService = hostLifetime is WindowsServiceLifetime; IsStarting = true; - // net6.0 will return Sonarr.dll for entry assembly, we need the actual + // net8.0 will return Sonarr.dll for entry assembly, we need the actual // executable name (Sonarr on linux). On mono this will return the location of // the mono executable itself, which is not what we want. var entry = Process.GetCurrentProcess().MainModule; diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 9eb4cc1e4..678e16548 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -1,12 +1,15 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; @@ -28,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers private readonly ICached _httpClientCache; private readonly ICached _credentialCache; + private readonly Logger _logger; + public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, ICertificateValidationService certificateValidationService, IUserAgentBuilder userAgentBuilder, - ICacheManager cacheManager) + ICacheManager cacheManager, + Logger logger) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; @@ -41,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers _httpClientCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher)); _credentialCache = cacheManager.GetCache(typeof(ManagedHttpDispatcher), "credentialcache"); + + _logger = logger; } public async Task GetResponseAsync(HttpRequest request, CookieContainer cookies) @@ -247,7 +255,27 @@ namespace NzbDrone.Common.Http.Dispatchers return _credentialCache.Get("credentialCache", () => new CredentialCache()); } - private static async ValueTask onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) + private bool HasRoutableIPv4Address() + { + // Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses + try + { + var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces(); + + return networkInterfaces.Any(ni => + ni.OperationalStatus == OperationalStatus.Up && + ni.GetIPProperties().UnicastAddresses.Any(ip => + ip.Address.AddressFamily == AddressFamily.InterNetwork && + !IPAddress.IsLoopback(ip.Address))); + } + catch (Exception e) + { + _logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message); + return true; + } + } + + private async ValueTask onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { // Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way. // This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6. @@ -270,10 +298,10 @@ namespace NzbDrone.Common.Http.Dispatchers } catch { - // very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt. - // note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance) - // but in the interest of keeping this implementation simple, this is acceptable. - useIPv6 = false; + // Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections. + var routableIPv4 = HasRoutableIPv4Address(); + _logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled"); + useIPv6 = !routableIPv4; } finally { diff --git a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs index 12d027afa..c2b496302 100644 --- a/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs +++ b/src/NzbDrone.Common/Instrumentation/CleanseLogMessage.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation new (@"/fetch/[a-z0-9]{32}/(?[a-z0-9]{32})", RegexOptions.Compiled), new (@"getnzb.*?(?<=\?|&)(r)=(?[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), new (@"\b(\w*)?(_?(?[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase), + new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Trackers Announce Keys; Designed for Qbit Json; should work for all in theory new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?[a-z0-9]{16,})|(?[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase), diff --git a/src/NzbDrone.Common/Sonarr.Common.csproj b/src/NzbDrone.Common/Sonarr.Common.csproj index 831798a2e..5ea3d3da9 100644 --- a/src/NzbDrone.Common/Sonarr.Common.csproj +++ b/src/NzbDrone.Common/Sonarr.Common.csproj @@ -1,25 +1,25 @@  - net6.0 + net8.0 ISMUSL - - - + + + - - - - + + + + - + - + - + diff --git a/src/NzbDrone.Console/Sonarr.Console.csproj b/src/NzbDrone.Console/Sonarr.Console.csproj index aef81527d..c383c33be 100644 --- a/src/NzbDrone.Console/Sonarr.Console.csproj +++ b/src/NzbDrone.Console/Sonarr.Console.csproj @@ -1,7 +1,7 @@  Exe - net6.0 + net8.0 ..\NzbDrone.Host\Sonarr.ico diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index a55bd51a4..8f8b7723c 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework Mocker.SetConstant(new HttpProxySettingsProvider(Mocker.Resolve())); Mocker.SetConstant(new ManagedWebProxyFactory(Mocker.Resolve())); Mocker.SetConstant(new X509CertificateValidationService(Mocker.Resolve(), TestLogger)); - Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve())); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new HttpClient(Array.Empty(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new SonarrCloudRequestBuilder()); } diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs index a26f08a67..99b6676be 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs @@ -1,6 +1,9 @@ +using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Tags; @@ -45,5 +48,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); AllStoredModels.Should().HaveCount(1); } + + [Test] + public void should_not_delete_used_auto_tagging_tag_specification_tags() + { + var tags = Builder + .CreateListOfSize(2) + .All() + .With(x => x.Id = 0) + .BuildList(); + Db.InsertMany(tags); + + var autoTags = Builder.CreateListOfSize(1) + .All() + .With(x => x.Id = 0) + .With(x => x.Specifications = new List + { + new TagSpecification + { + Name = "Test", + Value = tags[0].Id + } + }) + .BuildList(); + + Mocker.GetMock().Setup(s => s.All()) + .Returns(autoTags); + + Subject.Clean(); + AllStoredModels.Should().HaveCount(1); + } } } diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index d9702c24b..26e8cab7e 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -120,6 +120,17 @@ namespace NzbDrone.Core.Test.ImportListTests private void WithImdbId() { _list1Series.First().ImdbId = "tt0496424"; + + Mocker.GetMock() + .Setup(s => s.SearchForNewSeriesByImdbId(_list1Series.First().ImdbId)) + .Returns( + Builder + .CreateListOfSize(1) + .All() + .With(s => s.Title = "Breaking Bad") + .With(s => s.TvdbId = 81189) + .Build() + .ToList()); } private void WithExistingSeries() @@ -342,6 +353,7 @@ namespace NzbDrone.Core.Test.ImportListTests public void should_add_new_series_from_single_list_to_library() { _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithTvdbId(); WithList(1, true); WithCleanLevel(ListSyncLevelType.Disabled); @@ -358,6 +370,7 @@ namespace NzbDrone.Core.Test.ImportListTests _importListFetch.Series.ForEach(m => m.ImportListId = 1); _importListFetch.Series.AddRange(_list2Series); + WithTvdbId(); WithList(1, true); WithList(2, true); @@ -376,6 +389,7 @@ namespace NzbDrone.Core.Test.ImportListTests _importListFetch.Series.ForEach(m => m.ImportListId = 1); _importListFetch.Series.AddRange(_list2Series); + WithTvdbId(); WithList(1, true); WithList(2, false); @@ -422,12 +436,17 @@ namespace NzbDrone.Core.Test.ImportListTests public void should_search_by_imdb_if_series_title_and_series_imdb() { _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); WithImdbId(); + Subject.Execute(_commandAll); Mocker.GetMock() .Verify(v => v.SearchForNewSeriesByImdbId(It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(v => v.AddSeries(It.Is>(t => t.Count == 1), It.IsAny())); } [Test] @@ -498,5 +517,18 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock() .Verify(v => v.All(), Times.Never); } + + [Test] + public void should_not_add_if_tvdbid_is_0() + { + _importListFetch.Series.ForEach(m => m.ImportListId = 1); + WithList(1, true); + WithExcludedSeries(); + + Subject.Execute(_commandAll); + + Mocker.GetMock() + .Verify(v => v.AddSeries(It.Is>(t => t.Count == 0), It.IsAny())); + } } } diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs new file mode 100644 index 000000000..e3e8b848c --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHashFixture.cs @@ -0,0 +1,83 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + [TestFixture] + public class AggregateReleaseHashFixture : CoreTest + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew().Build(); + } + + [Test] + public void should_prefer_file() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCDEFGH]"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("ABCDEFGH"); + } + + [Test] + public void should_fallback_to_downloadclient() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC) [ABCD1234]"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("ABCD1234"); + } + + [Test] + public void should_fallback_to_folder() + { + var fileEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var downloadClientEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 (1280x720 10bit AAC)"); + var folderEpisodeInfo = Parser.Parser.ParseTitle("[DHD] Series Title! - 08 [12345678]"); + var localEpisode = new LocalEpisode + { + FileEpisodeInfo = fileEpisodeInfo, + FolderEpisodeInfo = folderEpisodeInfo, + DownloadClientEpisodeInfo = downloadClientEpisodeInfo, + Path = @"C:\Test\Unsorted TV\Series.Title.S01\Series.Title.S01E01.WEB-DL.mkv".AsOsAgnostic(), + Series = _series + }; + + Subject.Aggregate(localEpisode, null); + + localEpisode.ReleaseHash.Should().Be("12345678"); + } + } +} diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs index d5e4a472a..201537188 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfoFixture.cs @@ -9,13 +9,14 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators [TestFixture] public class AggregateSubtitleInfoFixture : CoreTest { - [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")] - [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")] - [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")] - [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass")] - [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass")] - [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass")] - public void should_do_basic_parse(string relativePath, string originalFilePath, string path) + [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)] + [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)] + [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)] + [TestCase("Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 5.1].mkv", "", "Name (2020) - S01E20 - [FLAC 2.0].fra.ass", "Name (2020)/Season 1/Name (2020) - S01E20 - [FLAC 2.0].mkv")] + [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].default.eng.forced.ass", null)] + [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].eng.default.ass", null)] + [TestCase("", "Name (2020)/Season 1/Name (2020) - S01E20 - [AAC 2.0].mkv", "Name (2020) - S01E20 - [AAC 2.0].fra.ass", null)] + public void should_do_basic_parse(string relativePath, string originalFilePath, string path, string fileNameBeforeRename) { var episodeFile = new EpisodeFile { @@ -23,7 +24,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators OriginalFilePath = originalFilePath }; - var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path); + var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, fileNameBeforeRename); subtitleTitleInfo.Title.Should().BeNull(); subtitleTitleInfo.Copy.Should().Be(0); @@ -40,7 +41,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators RelativePath = relativePath }; - var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path); + var subtitleTitleInfo = Subject.CleanSubtitleTitleInfo(episodeFile, path, null); subtitleTitleInfo.LanguageTags.Should().NotContain("default"); } diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs new file mode 100644 index 000000000..b07fbedbe --- /dev/null +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/CustomFormatsFixture.cs @@ -0,0 +1,146 @@ +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.Organizer; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests +{ + [TestFixture] + + public class CustomFormatsFixture : CoreTest + { + private Series _series; + private Episode _episode1; + private EpisodeFile _episodeFile; + private NamingConfig _namingConfig; + + private List _customFormats; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .With(s => s.Title = "South Park") + .Build(); + + _namingConfig = NamingConfig.Default; + _namingConfig.RenameEpisodes = true; + + Mocker.GetMock() + .Setup(c => c.GetConfig()).Returns(_namingConfig); + + _episode1 = Builder.CreateNew() + .With(e => e.Title = "City Sushi") + .With(e => e.SeasonNumber = 15) + .With(e => e.EpisodeNumber = 6) + .With(e => e.AbsoluteEpisodeNumber = 100) + .Build(); + + _episodeFile = new EpisodeFile { Quality = new QualityModel(Quality.HDTV720p), ReleaseGroup = "SonarrTest" }; + + _customFormats = new List() + { + new CustomFormat() + { + Name = "INTERNAL", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "AMZN", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "NAME WITH SPACES", + IncludeCustomFormatWhenRenaming = true + }, + new CustomFormat() + { + Name = "NotIncludedFormat", + IncludeCustomFormatWhenRenaming = false + } + }; + + Mocker.GetMock() + .Setup(v => v.Get(Moq.It.IsAny())) + .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); + } + + [TestCase("{Custom Formats}", "INTERNAL AMZN NAME WITH SPACES")] + public void should_replace_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Formats}", "")] + public void should_replace_custom_formats_with_no_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: new List()) + .Should().Be(expected); + } + + [TestCase("{Custom Formats:-INTERNAL}", "AMZN NAME WITH SPACES")] + [TestCase("{Custom Formats:-NAME WITH SPACES}", "INTERNAL AMZN")] + [TestCase("{Custom Formats:-INTERNAL,NAME WITH SPACES}", "AMZN")] + [TestCase("{Custom Formats:INTERNAL}", "INTERNAL")] + [TestCase("{Custom Formats:NAME WITH SPACES}", "NAME WITH SPACES")] + [TestCase("{Custom Formats:INTERNAL,NAME WITH SPACES}", "INTERNAL NAME WITH SPACES")] + public void should_replace_custom_formats_with_filtered_names(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Formats:-}", "{Custom Formats:-}")] + [TestCase("{Custom Formats:}", "{Custom Formats:}")] + public void should_not_replace_custom_formats_due_to_invalid_token(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Format}", "")] + [TestCase("{Custom Format:INTERNAL}", "INTERNAL")] + [TestCase("{Custom Format:AMZN}", "AMZN")] + [TestCase("{Custom Format:NAME WITH SPACES}", "NAME WITH SPACES")] + [TestCase("{Custom Format:DOESNOTEXIST}", "")] + [TestCase("{Custom Format:INTERNAL} - {Custom Format:AMZN}", "INTERNAL - AMZN")] + [TestCase("{Custom Format:AMZN} - {Custom Format:INTERNAL}", "AMZN - INTERNAL")] + public void should_replace_custom_format(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: _customFormats) + .Should().Be(expected); + } + + [TestCase("{Custom Format}", "")] + [TestCase("{Custom Format:INTERNAL}", "")] + [TestCase("{Custom Format:AMZN}", "")] + public void should_replace_custom_format_with_no_custom_formats(string format, string expected) + { + _namingConfig.StandardEpisodeFormat = format; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile, customFormats: new List()) + .Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs index 521d2d8d9..3b0cdb0af 100644 --- a/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs +++ b/src/NzbDrone.Core.Test/OrganizerTests/FileNameBuilderTests/FileNameBuilderFixture.cs @@ -991,6 +991,28 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests result.Should().EndWith("HDR"); } + [Test] + public void should_replace_release_hash_with_stored_hash() + { + _namingConfig.StandardEpisodeFormat = "{Release Hash}"; + + _episodeFile.ReleaseHash = "ABCDEFGH"; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be("ABCDEFGH"); + } + + [Test] + public void should_replace_null_release_hash_with_empty_string() + { + _namingConfig.StandardEpisodeFormat = "{Release Hash}"; + + _episodeFile.ReleaseHash = null; + + Subject.BuildFileName(new List { _episode1 }, _series, _episodeFile) + .Should().Be(string.Empty); + } + private void GivenMediaInfoModel(string videoCodec = "h264", string audioCodec = "dts", int audioChannels = 6, diff --git a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs index 37c54215c..30c2907f2 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AbsoluteEpisodeNumberParserFixture.cs @@ -132,6 +132,9 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Naruto-Kun.Hu] Dr Series S3 - 21 [1080p]", "Dr Series S3", 21, 0, 0)] [TestCase("[Naruto-Kun.Hu] Series Title - 12 [1080p].mkv", "Series Title", 12, 0, 0)] [TestCase("[Naruto-Kun.Hu] Anime Triangle - 08 [1080p].mkv", "Anime Triangle", 8, 0, 0)] + [TestCase("[Mystic Z-Team] Series Title Super - Episode 013 VF - Non-censuré [720p].mp4", "Series Title Super", 13, 0, 0)] + [TestCase("Series Title Kai Episodio 13 Audio Latino", "Series Title Kai", 13, 0, 0)] + [TestCase("Series_Title_2_[01]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 0, 0)] // [TestCase("", "", 0, 0, 0)] public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber) @@ -177,6 +180,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Erai-raws] Series-Title! 2 - 01~10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)] [TestCase("[Erai-raws] Series Title! - 01 ~ 10 [1080p][Multiple Subtitle]", "Series Title!", 1, 10)] [TestCase("[Erai-raws] Series-Title! 2 - 01 ~ 10 [1080p][Multiple Subtitle]", "Series-Title! 2", 1, 10)] + [TestCase("Series_Title_2_[01-05]_[AniLibria_TV]_[WEBRip_1080p]", "Series Title 2", 1, 5)] // [TestCase("", "", 1, 2)] public void should_parse_multi_episode_absolute_numbers(string postTitle, string title, int firstAbsoluteEpisodeNumber, int lastAbsoluteEpisodeNumber) diff --git a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs index 5a1f8bef4..52794f643 100644 --- a/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/AnimeMetadataParserFixture.cs @@ -22,12 +22,42 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")] [TestCase("[ACX]Series Title 01 Episode Name [Kosaka] [9C57891E].mkv", "ACX", "9C57891E")] [TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) [59B3F2EA].mkv", "S-T-D", "59B3F2EA")] - public void should_parse_absolute_numbers(string postTitle, string subGroup, string hash) + + // These tests are dupes of the above, except with parenthesized hashes instead of square bracket + [TestCase("[SubDESU]_Show_Title_DxD_07_(1280x720_x264-AAC)_(6B7FD717)", "SubDESU", "6B7FD717")] + [TestCase("[Chihiro]_Show_Title!!_-_06_[848x480_H.264_AAC](859EEAFA)", "Chihiro", "859EEAFA")] + [TestCase("[Underwater]_Show_Title_-_12_(720p)_(5C7BC4F9)", "Underwater", "5C7BC4F9")] + [TestCase("[HorribleSubs]_Show_Title_-_33_[720p]", "HorribleSubs", "")] + [TestCase("[HorribleSubs] Show-Title - 13 [1080p].mkv", "HorribleSubs", "")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].(C65D4B1F).mkv", "Doremi", "C65D4B1F")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31[1280x720].(C65D4B1F)", "Doremi", "C65D4B1F")] + [TestCase("[Doremi].Show.Title.5.Go.Go!.31.[1280x720].mkv", "Doremi", "")] + [TestCase("[K-F] Series Title 214", "K-F", "")] + [TestCase("[K-F] Series Title S10E14 214", "K-F", "")] + [TestCase("[K-F] Series Title 10x14 214", "K-F", "")] + [TestCase("[K-F] Series Title 214 10x14", "K-F", "")] + [TestCase("Series Title - 031 - The Resolution to Kill [Lunar].avi", "Lunar", "")] + [TestCase("[ACX]Series Title 01 Episode Name [Kosaka] (9C57891E).mkv", "ACX", "9C57891E")] + [TestCase("[S-T-D] Series Title! - 06 (1280x720 10bit AAC) (59B3F2EA).mkv", "S-T-D", "59B3F2EA")] + public void should_parse_releasegroup_and_hash(string postTitle, string subGroup, string hash) { var result = Parser.Parser.ParseTitle(postTitle); result.Should().NotBeNull(); result.ReleaseGroup.Should().Be(subGroup); result.ReleaseHash.Should().Be(hash); } + + [TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [8B00F2EA].mkv", "8B00F2EA")] + [TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [10BBF2EA].mkv", "10BBF2EA")] + [TestCase("[DHD] Series Title! - 08 (1280x720 10bit AAC) [008BF28B].mkv", "008BF28B")] + [TestCase("[DHD] Series Title! - 10 (1280x720 10bit AAC) [000BF10B].mkv", "000BF10B")] + [TestCase("[DHD] Series Title! - 08 (1280x720 8bit AAC) [8B8BF2EA].mkv", "8B8BF2EA")] + [TestCase("[DHD] Series Title! - 10 (1280x720 8bit AAC) [10B10BEA].mkv", "10B10BEA")] + public void should_parse_release_hashes_with_10b_or_8b(string postTitle, string hash) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Should().NotBeNull(); + result.ReleaseHash.Should().Be(hash); + } } } diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index af422fa1b..dce7fafc6 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -444,6 +444,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Name (2020) - S01E20 - [AAC 2.0].ru-something-else.srt", new string[0], "something-else", "Russian")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].Full Subtitles.eng.ass", new string[0], "Full Subtitles", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle - 1.en.ass", new string[0], "mytitle", "English")] + [TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle 1.en.ass", new string[0], "mytitle 1", "English")] [TestCase("Name (2020) - S01E20 - [AAC 2.0].mytitle.en.ass", new string[0], "mytitle", "English")] public void should_parse_title_and_tags(string postTitle, string[] expectedTags, string expectedTitle, string expectedLanguage) { diff --git a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs index e3c098f4f..22c607198 100644 --- a/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/MultiEpisodeParserFixture.cs @@ -77,6 +77,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Series Title (S15E06-08) City Sushi", "Series Title", 15, new[] { 6, 7, 8 })] [TestCase("Босх: Спадок (S2E1-4) / Series: Legacy (S2E1-4) (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })] [TestCase("Босх: Спадок / Series: Legacy / S2E1-4 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, new[] { 1, 2, 3, 4 })] + [TestCase("Series Title - S26E96-97-98-99-100 - Episode 5931 + Episode 5932 + Episode 5933 + Episode 5934 + Episode 5935", "Series Title", 26, new[] { 96, 97, 98, 99, 100 })] // [TestCase("", "", , new [] { })] public void should_parse_multiple_episodes(string postTitle, string title, int season, int[] episodes) diff --git a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs index 03bc64fa5..f2c8d2a84 100644 --- a/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/PathParserFixture.cs @@ -29,6 +29,10 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase(@"C:\Test\Series\Season 01\1 Pilot (1080p HD).mkv", 1, 1)] [TestCase(@"C:\Test\Series\Season 1\02 Honor Thy Father (1080p HD).m4v", 1, 2)] [TestCase(@"C:\Test\Series\Season 1\2 Honor Thy Developer (1080p HD).m4v", 1, 2)] + [TestCase(@"C:\Test\Series\Season 2 - Total Series Action\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)] + [TestCase(@"C:\Test\Series\Season 2\01. Total Series Action - Episode 1 - Monster Cash.mkv", 2, 1)] + [TestCase(@"C:\Test\Series\Season 1\02.04.24 - S01E01 - The Rabbit Hole", 1, 1)] + [TestCase(@"C:\Test\Series\Season 1\8 Series Rules - S01E01 - Pilot", 1, 1)] // [TestCase(@"C:\series.state.S02E04.720p.WEB-DL.DD5.1.H.264\73696S02-04.mkv", 2, 4)] //Gets treated as S01E04 (because it gets parsed as anime); 2020-01 broken test case: Expected result.EpisodeNumbers to contain 1 item(s), but found 0 public void should_parse_from_path(string path, int season, int episode) @@ -45,6 +49,7 @@ namespace NzbDrone.Core.Test.ParserTests } [TestCase("01-03\\The Series Title (2010) - 1x01-02-03 - Episode Title HDTV-720p Proper", "The Series Title (2010)", 1, new[] { 1, 2, 3 })] + [TestCase("Season 2\\E05-06 - Episode Title HDTV-720p Proper", "", 2, new[] { 5, 6 })] public void should_parse_multi_episode_from_path(string path, string title, int season, int[] episodes) { var result = Parser.Parser.ParsePath(path.AsOsAgnostic()); diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index b2d13a262..b22d7c43a 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -44,6 +44,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("[Erai-raws] Series - 0955 ~ 1005 [1080p]", "Erai-raws")] [TestCase("[Exiled-Destiny] Series Title", "Exiled-Destiny")] [TestCase("Series.Title.S01E09.1080p.DSNP.WEB-DL.DDP2.0.H.264-VARYG", "VARYG")] + [TestCase("Stargate SG-1 (1997) - S01E01-02 - Children of the Gods (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")] // [TestCase("", "")] public void should_parse_release_group(string title, string expected) diff --git a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj index 6a1477da4..91d89a8a0 100644 --- a/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Sonarr.Core.Test.csproj @@ -1,9 +1,9 @@  - net6.0 + net8.0 - + diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs index 6f90c9716..fe8e521b3 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Test.Framework; @@ -14,14 +16,20 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests [TestFixture] public class HandleEpisodeFileDeletedFixture : CoreTest { + private Series _series; private EpisodeFile _episodeFile; private List _episodes; [SetUp] public void Setup() { + _series = Builder + .CreateNew() + .Build(); + _episodeFile = Builder .CreateNew() + .With(e => e.SeriesId = _series.Id) .Build(); } @@ -30,6 +38,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests _episodes = Builder .CreateListOfSize(1) .All() + .With(e => e.SeriesId = _series.Id) .With(e => e.Monitored = true) .Build() .ToList(); @@ -44,6 +53,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests _episodes = Builder .CreateListOfSize(2) .All() + .With(e => e.SeriesId = _series.Id) .With(e => e.Monitored = true) .Build() .ToList(); @@ -85,9 +95,31 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests .Returns(true); Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); + Subject.HandleAsync(new SeriesScannedEvent(_series, new List())); + + Mocker.GetMock() + .Verify(v => v.SetMonitored(It.IsAny>(), false), Times.Once()); + } + + [Test] + public void should_leave_monitored_if_autoUnmonitor_is_true_and_missing_episode_is_replaced() + { + GivenSingleEpisodeFile(); + + var newEpisodeFile = _episodeFile.JsonClone(); + newEpisodeFile.Id = 123; + newEpisodeFile.Episodes = new LazyLoaded>(_episodes); + + Mocker.GetMock() + .SetupGet(s => s.AutoUnmonitorPreviouslyDownloadedEpisodes) + .Returns(true); + + Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); + Subject.Handle(new EpisodeFileAddedEvent(newEpisodeFile)); + Subject.HandleAsync(new SeriesScannedEvent(_series, new List())); Mocker.GetMock() - .Verify(v => v.ClearFileId(It.IsAny(), true), Times.Once()); + .Verify(v => v.SetMonitored(It.IsAny>(), false), Times.Never()); } [Test] diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index a0a1896e0..398376117 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -85,7 +85,8 @@ namespace NzbDrone.Core.Annotations Device, TagSelect, RootFolder, - QualityProfile + QualityProfile, + SeriesTag } public enum HiddenType diff --git a/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs b/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs new file mode 100644 index 000000000..c736f8899 --- /dev/null +++ b/src/NzbDrone.Core/AutoTagging/Specifications/TagSpecification.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.AutoTagging.Specifications +{ + public class TagSpecificationValidator : AbstractValidator + { + public TagSpecificationValidator() + { + RuleFor(c => c.Value).GreaterThan(0); + } + } + + public class TagSpecification : AutoTaggingSpecificationBase + { + private static readonly TagSpecificationValidator Validator = new (); + + public override int Order => 1; + public override string ImplementationName => "Tag"; + + [FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.SeriesTag)] + public int Value { get; set; } + + protected override bool IsSatisfiedByWithoutNegate(Series series) + { + return series.Tags.Contains(Value); + } + + public override NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs index 51e61e287..8c58b1e07 100644 --- a/src/NzbDrone.Core/CustomFormats/CustomFormat.cs +++ b/src/NzbDrone.Core/CustomFormats/CustomFormat.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; diff --git a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs similarity index 92% rename from src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs rename to src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs index acc6d9c4d..d14a6e041 100644 --- a/src/NzbDrone.Core/CustomFormats/Specifications/SeasonPackSpecification.cs +++ b/src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTypeSpecification.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.CustomFormats { - public class SeasonPackSpecificationValidator : AbstractValidator + public class SeasonPackSpecificationValidator : AbstractValidator { public SeasonPackSpecificationValidator() { @@ -20,7 +20,7 @@ namespace NzbDrone.Core.CustomFormats } } - public class SeasonPackSpecification : CustomFormatSpecificationBase + public class ReleaseTypeSpecification : CustomFormatSpecificationBase { private static readonly SeasonPackSpecificationValidator Validator = new (); diff --git a/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs b/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs new file mode 100644 index 000000000..887d35cda --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/204_add_release_hash.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Data; +using System.IO; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(204)] + public class add_add_release_hash : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles").AddColumn("ReleaseHash").AsString().Nullable(); + + Execute.WithConnection(UpdateEpisodeFiles); + } + + private void UpdateEpisodeFiles(IDbConnection conn, IDbTransaction tran) + { + var updates = new List(); + + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"SceneName\", \"RelativePath\", \"OriginalFilePath\" FROM \"EpisodeFiles\""; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var id = reader.GetInt32(0); + var sceneName = reader[1] as string; + var relativePath = reader[2] as string; + var originalFilePath = reader[3] as string; + + ParsedEpisodeInfo parsedEpisodeInfo = null; + + var originalTitle = sceneName; + + if (originalTitle.IsNullOrWhiteSpace() && originalFilePath.IsNotNullOrWhiteSpace()) + { + originalTitle = Path.GetFileNameWithoutExtension(originalFilePath); + } + + if (originalTitle.IsNotNullOrWhiteSpace()) + { + parsedEpisodeInfo = Parser.Parser.ParseTitle(originalTitle); + } + + if (parsedEpisodeInfo == null || parsedEpisodeInfo.ReleaseHash.IsNullOrWhiteSpace()) + { + parsedEpisodeInfo = Parser.Parser.ParseTitle(Path.GetFileNameWithoutExtension(relativePath)); + } + + if (parsedEpisodeInfo != null && parsedEpisodeInfo.ReleaseHash.IsNotNullOrWhiteSpace()) + { + updates.Add(new + { + Id = id, + ReleaseHash = parsedEpisodeInfo.ReleaseHash + }); + } + } + } + + if (updates.Count > 0) + { + var updateEpisodeFilesSql = "UPDATE \"EpisodeFiles\" SET \"ReleaseHash\" = @ReleaseHash WHERE \"Id\" = @Id"; + conn.Execute(updateEpisodeFilesSql, updates, transaction: tran); + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs b/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs new file mode 100644 index 000000000..23e40e0e5 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/205_rename_season_pack_spec.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(205)] + public class rename_season_pack_spec : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.Sql("UPDATE \"CustomFormats\" SET \"Specifications\" = REPLACE(\"Specifications\", 'SeasonPackSpecification', 'ReleaseTypeSpecification')"); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs index 0b39ebc01..ea670cfd6 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeProxy.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Dynamic; using System.Linq; using System.Net; using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Common.Serializer; @@ -101,11 +103,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings) { - var options = new - { - add_paused = settings.AddPaused, - remove_at_ratio = false - }; + dynamic options = new ExpandoObject(); + + options.add_paused = settings.AddPaused; + options.remove_at_ratio = false; + + if (settings.DownloadDirectory.IsNotNullOrWhiteSpace()) + { + options.download_location = settings.DownloadDirectory; + } + + if (settings.CompletedDirectory.IsNotNullOrWhiteSpace()) + { + options.move_completed_path = settings.CompletedDirectory; + options.move_completed = true; + } var response = ProcessRequest(settings, "core.add_torrent_magnet", magnetLink, options); @@ -114,11 +126,21 @@ namespace NzbDrone.Core.Download.Clients.Deluge public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings) { - var options = new - { - add_paused = settings.AddPaused, - remove_at_ratio = false - }; + dynamic options = new ExpandoObject(); + + options.add_paused = settings.AddPaused; + options.remove_at_ratio = false; + + if (settings.DownloadDirectory.IsNotNullOrWhiteSpace()) + { + options.download_location = settings.DownloadDirectory; + } + + if (settings.CompletedDirectory.IsNotNullOrWhiteSpace()) + { + options.move_completed_path = settings.CompletedDirectory; + options.move_completed = true; + } var response = ProcessRequest(settings, "core.add_torrent_file", filename, fileContent, options); diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs index 03f266189..f18643510 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/DelugeSettings.cs @@ -61,6 +61,12 @@ namespace NzbDrone.Core.Download.Clients.Deluge [FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)] public bool AddPaused { get; set; } + [FieldDefinition(10, Label = "DownloadClientDelugeSettingsDirectory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryHelpText")] + public string DownloadDirectory { get; set; } + + [FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")] + public string CompletedDirectory { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index e8917a0c9..b072b193d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -388,16 +388,20 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - var minimumRetention = 60 * 24 * 14; - return new DownloadClientInfo { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }, - RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles) + RemovesCompletedDownloads = RemovesCompletedDownloads(config) }; } + private bool RemovesCompletedDownloads(QBittorrentPreferences config) + { + var minimumRetention = 60 * 24 * 14; // 14 days in minutes + return (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles); + } + protected override void Test(List failures) { failures.AddIfNotNull(TestConnection()); @@ -448,7 +452,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Complain if qBittorrent is configured to remove torrents on max ratio var config = Proxy.GetConfig(Settings); - if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)) + if (RemovesCompletedDownloads(config)) { return new NzbDroneValidationFailure(string.Empty, _localizationService.GetLocalizedString("DownloadClientQbittorrentValidationRemovesAtRatioLimit")) { diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 5705e33a3..fd91a3833 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -139,12 +139,14 @@ namespace NzbDrone.Core.Download.Clients.RTorrent // Ignore torrents with an empty path if (torrent.Path.IsNullOrWhiteSpace()) { + _logger.Warn("Torrent '{0}' has an empty download path and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name); continue; } if (torrent.Path.StartsWith(".")) { - throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent."); + _logger.Warn("Torrent '{0}' has a download path starting with '.' and will not be processed. Adjust this to an absolute path in rTorrent", torrent.Name); + continue; } var item = new DownloadClientItem(); diff --git a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs index ae17b44c6..d357c5ba6 100644 --- a/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs +++ b/src/NzbDrone.Core/Extras/ExistingExtraFileService.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Extras { public interface IExistingExtraFiles { - List ImportExtraFiles(Series series, List possibleExtraFiles); + List ImportExtraFiles(Series series, List possibleExtraFiles, string fileNameBeforeRename); } public class ExistingExtraFileService : IExistingExtraFiles, IHandle @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Extras _logger = logger; } - public List ImportExtraFiles(Series series, List possibleExtraFiles) + public List ImportExtraFiles(Series series, List possibleExtraFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing extra files in {0}", series.Path); @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Extras foreach (var existingExtraFileImporter in _existingExtraFileImporters) { - var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles); + var imported = existingExtraFileImporter.ProcessFiles(series, possibleExtraFiles, importedFiles, fileNameBeforeRename); importedFiles.AddRange(imported.Select(f => Path.Combine(series.Path, f.RelativePath))); } @@ -45,7 +45,7 @@ namespace NzbDrone.Core.Extras { var series = message.Series; var possibleExtraFiles = message.PossibleExtraFiles; - var importedFiles = ImportExtraFiles(series, possibleExtraFiles); + var importedFiles = ImportExtraFiles(series, possibleExtraFiles, null); _logger.Info("Found {0} possible extra files, imported {1} files.", possibleExtraFiles.Count, importedFiles.Count); } diff --git a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs index ad14b60a5..97b85d80f 100644 --- a/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs +++ b/src/NzbDrone.Core/Extras/IImportExistingExtraFiles.cs @@ -7,6 +7,6 @@ namespace NzbDrone.Core.Extras public interface IImportExistingExtraFiles { int Order { get; } - IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles, string fileNameBeforeRename); } } diff --git a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs index a2dddaa69..fdfc7e67e 100644 --- a/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs +++ b/src/NzbDrone.Core/Extras/ImportExistingExtraFilesBase.cs @@ -19,12 +19,21 @@ namespace NzbDrone.Core.Extras } public abstract int Order { get; } - public abstract IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles); + public abstract IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles, string fileNameBeforeRename); - public virtual ImportExistingExtraFileFilterResult FilterAndClean(Series series, List filesOnDisk, List importedFiles) + public virtual ImportExistingExtraFileFilterResult FilterAndClean(Series series, List filesOnDisk, List importedFiles, bool keepExistingEntries) { var seriesFiles = _extraFileService.GetFilesBySeries(series.Id); + if (keepExistingEntries) + { + var incompleteImports = seriesFiles.IntersectBy(f => Path.Combine(series.Path, f.RelativePath), filesOnDisk, i => i, PathEqualityComparer.Instance).Select(f => f.Id); + + _extraFileService.DeleteMany(incompleteImports); + + return Filter(series, filesOnDisk, importedFiles, new List()); + } + Clean(series, filesOnDisk, importedFiles, seriesFiles); return Filter(series, filesOnDisk, importedFiles, seriesFiles); diff --git a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs index 8ceb31a7f..373282259 100644 --- a/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs +++ b/src/NzbDrone.Core/Extras/Metadata/ExistingMetadataImporter.cs @@ -33,12 +33,12 @@ namespace NzbDrone.Core.Extras.Metadata public override int Order => 0; - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing metadata in {0}", series.Path); var metadataFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleMetadataFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs index 572149965..ea8d021de 100644 --- a/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs +++ b/src/NzbDrone.Core/Extras/Others/ExistingOtherExtraImporter.cs @@ -28,12 +28,12 @@ namespace NzbDrone.Core.Extras.Others public override int Order => 2; - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing extra files in {0}", series.Path); var extraFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleExtraFile in filterResult.FilesOnDisk) { diff --git a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs index 6c5a5481e..631c92be3 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/ExistingSubtitleImporter.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -28,12 +29,12 @@ namespace NzbDrone.Core.Extras.Subtitles public override int Order => 1; - public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles) + public override IEnumerable ProcessFiles(Series series, List filesOnDisk, List importedFiles, string fileNameBeforeRename) { _logger.Debug("Looking for existing subtitle files in {0}", series.Path); var subtitleFiles = new List(); - var filterResult = FilterAndClean(series, filesOnDisk, importedFiles); + var filterResult = FilterAndClean(series, filesOnDisk, importedFiles, fileNameBeforeRename is not null); foreach (var possibleSubtitleFile in filterResult.FilesOnDisk) { @@ -45,7 +46,8 @@ namespace NzbDrone.Core.Extras.Subtitles { FileEpisodeInfo = Parser.Parser.ParsePath(possibleSubtitleFile), Series = series, - Path = possibleSubtitleFile + Path = possibleSubtitleFile, + FileNameBeforeRename = fileNameBeforeRename }; try @@ -78,11 +80,11 @@ namespace NzbDrone.Core.Extras.Subtitles SeasonNumber = localEpisode.SeasonNumber, EpisodeFileId = firstEpisode.EpisodeFileId, RelativePath = series.Path.GetRelativePath(possibleSubtitleFile), - Language = localEpisode.SubtitleInfo.Language, - LanguageTags = localEpisode.SubtitleInfo.LanguageTags, - Title = localEpisode.SubtitleInfo.Title, + Language = localEpisode.SubtitleInfo?.Language ?? Language.Unknown, + LanguageTags = localEpisode.SubtitleInfo?.LanguageTags ?? new List(), + Title = localEpisode.SubtitleInfo?.Title, Extension = extension, - Copy = localEpisode.SubtitleInfo.Copy + Copy = localEpisode.SubtitleInfo?.Copy ?? 0 }; subtitleFiles.Add(subtitleFile); diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs index 4f0a6de05..c11f8bc13 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientRootFolderCheck.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.HealthCheck.Checks _localizationService.GetLocalizedString("DownloadClientRootFolderHealthCheckMessage", new Dictionary { { "downloadClientName", client.Definition.Name }, - { "path", folder.FullPath } + { "rootFolderPath", folder.FullPath } }), "#downloads-in-root-folder"); } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 9bd726ba3..c68d053f9 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Data; using System.Linq; using Dapper; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping.Housekeepers @@ -9,17 +11,24 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers public class CleanupUnusedTags : IHousekeepingTask { private readonly IMainDatabase _database; + private readonly IAutoTaggingRepository _autoTaggingRepository; - public CleanupUnusedTags(IMainDatabase database) + public CleanupUnusedTags(IMainDatabase database, IAutoTaggingRepository autoTaggingRepository) { _database = database; + _autoTaggingRepository = autoTaggingRepository; } public void Clean() { using var mapper = _database.OpenConnection(); - var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" } + var usedTags = new[] + { + "Series", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", + "AutoTagging", "DownloadClients" + } .SelectMany(v => GetUsedTags(v, mapper)) + .Concat(GetAutoTaggingTagSpecificationTags(mapper)) .Distinct() .ToArray(); @@ -37,10 +46,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers private int[] GetUsedTags(string table, IDbConnection mapper) { - return mapper.Query>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") + return mapper + .Query>( + $"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL") .SelectMany(x => x) .Distinct() .ToArray(); } + + private List GetAutoTaggingTagSpecificationTags(IDbConnection mapper) + { + var tags = new List(); + var autoTags = _autoTaggingRepository.All(); + + foreach (var autoTag in autoTags) + { + foreach (var specification in autoTag.Specifications) + { + if (specification is TagSpecification tagSpec) + { + tags.Add(tagSpec.Value); + } + } + } + + return tags; + } } } diff --git a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs index f2ecb26e7..adcb9b9d9 100644 --- a/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs +++ b/src/NzbDrone.Core/ImportLists/AniList/List/AniListImport.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; @@ -12,6 +13,7 @@ using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.Localization; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.ImportLists.AniList.List { @@ -153,5 +155,63 @@ namespace NzbDrone.Core.ImportLists.AniList.List return new ImportListFetchResult(CleanupListItems(releases), anyFailure); } + + protected override ValidationFailure TestConnection() + { + try + { + var parser = GetParser(); + var generator = GetRequestGenerator(); + var pageIndex = 1; + var continueTesting = true; + var hasResults = false; + + // Anilist caps the result list to 50 items at maximum per query, so the data must be pulled in batches. + // The number of pages are not known upfront, so the fetch logic must be changed to look at the returned page data. + do + { + var currentRequest = generator.GetRequest(pageIndex); + var response = FetchImportListResponse(currentRequest); + var page = parser.ParseResponse(response, out var pageInfo).ToList(); + + // Continue testing additional pages if all results were filtered out by 'Media' filters and there are additional pages + continueTesting = pageInfo.HasNextPage && page.Count == 0; + pageIndex = pageInfo.CurrentPage + 1; + hasResults = page.Count > 0; + } + while (continueTesting); + + if (!hasResults) + { + return new NzbDroneValidationFailure(string.Empty, + "No results were returned from your import list, please check your settings and the log for details.") + { IsWarning = true }; + } + } + catch (RequestLimitReachedException) + { + _logger.Warn("Request limit reached"); + } + catch (UnsupportedFeedException ex) + { + _logger.Warn(ex, "Import list feed is not supported"); + + return new ValidationFailure(string.Empty, "Import list feed is not supported: " + ex.Message); + } + catch (ImportListException ex) + { + _logger.Warn(ex, "Unable to connect to import list"); + + return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details."); + } + catch (Exception ex) + { + _logger.Warn(ex, "Unable to connect to import list"); + + return new ValidationFailure(string.Empty, $"Unable to connect to import list: {ex.Message}. Check the log surrounding this error for details."); + } + + return null; + } } } diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs index 09871fef3..2a9f0a9ec 100644 --- a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Events; @@ -9,6 +10,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions { ImportListExclusion Add(ImportListExclusion importListExclusion); List All(); + PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); ImportListExclusion Get(int id); ImportListExclusion FindByTvdbId(int tvdbId); @@ -54,6 +56,11 @@ namespace NzbDrone.Core.ImportLists.Exclusions return _repo.All().ToList(); } + public PagingSpec Paged(PagingSpec pagingSpec) + { + return _repo.GetPaged(pagingSpec); + } + public void HandleAsync(SeriesDeletedEvent message) { if (!message.AddImportListExclusion) diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 2c51ea6c0..291ecba27 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -190,6 +190,29 @@ namespace NzbDrone.Core.ImportLists item.Title = mappedSeries.Title; } + // Map by MyAniList ID if we have it + if (item.TvdbId <= 0 && item.MalId > 0) + { + var mappedSeries = _seriesSearchService.SearchForNewSeriesByMyAnimeListId(item.MalId) + .FirstOrDefault(); + + if (mappedSeries == null) + { + _logger.Debug("Rejected, unable to find matching TVDB ID for MAL ID: {0} [{1}]", item.MalId, item.Title); + + continue; + } + + item.TvdbId = mappedSeries.TvdbId; + item.Title = mappedSeries.Title; + } + + if (item.TvdbId == 0) + { + _logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title); + continue; + } + // Check to see if series excluded var excludedSeries = listExclusions.Where(s => s.TvdbId == item.TvdbId).SingleOrDefault(); @@ -202,7 +225,7 @@ namespace NzbDrone.Core.ImportLists // Break if Series Exists in DB if (existingTvdbIds.Any(x => x == item.TvdbId)) { - _logger.Debug("{0} [{1}] Rejected, Series Exists in DB", item.TvdbId, item.Title); + _logger.Debug("{0} [{1}] Rejected, series exists in database", item.TvdbId, item.Title); continue; } diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs new file mode 100644 index 000000000..1ba6898c9 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListImport.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using NLog; +using NzbDrone.Common.Cloud; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Localization; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public class MyAnimeListImport : HttpImportListBase + { + public const string OAuthPath = "oauth/myanimelist/authorize"; + public const string RedirectUriPath = "oauth/myanimelist/auth"; + public const string RenewUriPath = "oauth/myanimelist/renew"; + + public override string Name => "MyAnimeList"; + public override ImportListType ListType => ImportListType.Other; + public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6); + + private readonly IImportListRepository _importListRepository; + private readonly IHttpRequestBuilderFactory _requestBuilder; + + // This constructor the first thing that is called when sonarr creates a button + public MyAnimeListImport(IImportListRepository netImportRepository, IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, ISonarrCloudRequestBuilder requestBuilder, Logger logger) + : base(httpClient, importListStatusService, configService, parsingService, localizationService, logger) + { + _importListRepository = netImportRepository; + _requestBuilder = requestBuilder.Services; + } + + public override ImportListFetchResult Fetch() + { + if (Settings.Expires < DateTime.UtcNow.AddMinutes(5)) + { + RefreshToken(); + } + + return FetchItems(g => g.GetListItems()); + } + + // MAL OAuth info: https://myanimelist.net/blog.php?eid=835707 + // The whole process is handled through Sonarr's services. + public override object RequestAction(string action, IDictionary query) + { + if (action == "startOAuth") + { + var request = _requestBuilder.Create() + .Resource(OAuthPath) + .AddQueryParam("state", query["callbackUrl"]) + .AddQueryParam("redirect_uri", _requestBuilder.Create().Resource(RedirectUriPath).Build().Url.ToString()) + .Build(); + + return new + { + OauthUrl = request.Url.ToString() + }; + } + else if (action == "getOAuthToken") + { + return new + { + accessToken = query["access_token"], + expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])), + refreshToken = query["refresh_token"] + }; + } + + return new { }; + } + + public override IParseImportListResponse GetParser() + { + return new MyAnimeListParser(); + } + + public override IImportListRequestGenerator GetRequestGenerator() + { + return new MyAnimeListRequestGenerator() + { + Settings = Settings, + }; + } + + private void RefreshToken() + { + _logger.Trace("Refreshing Token"); + + Settings.Validate().Filter("RefreshToken").ThrowOnError(); + + var httpReq = _requestBuilder.Create() + .Resource(RenewUriPath) + .AddQueryParam("refresh_token", Settings.RefreshToken) + .Build(); + try + { + var httpResp = _httpClient.Get(httpReq); + + if (httpResp?.Resource != null) + { + var token = httpResp.Resource; + Settings.AccessToken = token.AccessToken; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); + Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken; + + if (Definition.Id > 0) + { + _importListRepository.UpdateSettings((ImportListDefinition)Definition); + } + } + } + catch (HttpRequestException) + { + _logger.Error("Error trying to refresh MAL access token."); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs new file mode 100644 index 000000000..23a74b1f4 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListParser.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public class MyAnimeListParser : IParseImportListResponse + { + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MyAnimeListParser)); + + public IList ParseResponse(ImportListResponse importListResponse) + { + var jsonResponse = Json.Deserialize(importListResponse.Content); + var series = new List(); + + foreach (var show in jsonResponse.Animes) + { + series.AddIfNotNull(new ImportListItemInfo + { + Title = show.AnimeListInfo.Title, + MalId = show.AnimeListInfo.Id + }); + } + + return series; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs new file mode 100644 index 000000000..7bf62254a --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListRequestGenerator.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public class MyAnimeListRequestGenerator : IImportListRequestGenerator + { + public MyAnimeListSettings Settings { get; set; } + + private static readonly Dictionary StatusMapping = new Dictionary + { + { MyAnimeListStatus.Watching, "watching" }, + { MyAnimeListStatus.Completed, "completed" }, + { MyAnimeListStatus.OnHold, "on_hold" }, + { MyAnimeListStatus.Dropped, "dropped" }, + { MyAnimeListStatus.PlanToWatch, "plan_to_watch" }, + }; + + public virtual ImportListPageableRequestChain GetListItems() + { + var pageableReq = new ImportListPageableRequestChain(); + + pageableReq.Add(GetSeriesRequest()); + + return pageableReq; + } + + private IEnumerable GetSeriesRequest() + { + var status = (MyAnimeListStatus)Settings.ListStatus; + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim()); + + requestBuilder.Resource("users/@me/animelist"); + requestBuilder.AddQueryParam("fields", "list_status"); + requestBuilder.AddQueryParam("limit", "1000"); + requestBuilder.Accept(HttpAccept.Json); + + if (status != MyAnimeListStatus.All && StatusMapping.TryGetValue(status, out var statusName)) + { + requestBuilder.AddQueryParam("status", statusName); + } + + var httpReq = new ImportListRequest(requestBuilder.Build()); + + httpReq.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.AccessToken}"); + + yield return httpReq; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs new file mode 100644 index 000000000..9c55eecd6 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListResponses.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public class MyAnimeListResponse + { + [JsonProperty("data")] + public List Animes { get; set; } + } + + public class MyAnimeListItem + { + [JsonProperty("node")] + public MyAnimeListItemInfo AnimeListInfo { get; set; } + + [JsonProperty("list_status")] + public MyAnimeListStatusResult ListStatus { get; set; } + } + + public class MyAnimeListStatusResult + { + public string Status { get; set; } + } + + public class MyAnimeListItemInfo + { + public int Id { get; set; } + public string Title { get; set; } + } + + public class MyAnimeListIds + { + [JsonProperty("mal_id")] + public int MalId { get; set; } + + [JsonProperty("thetvdb_id")] + public int TvdbId { get; set; } + } + + public class MyAnimeListAuthToken + { + [JsonProperty("token_type")] + public string TokenType { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs new file mode 100644 index 000000000..aad6257c8 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListSettings.cs @@ -0,0 +1,58 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public class MalSettingsValidator : AbstractValidator + { + public MalSettingsValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.AccessToken).NotEmpty() + .OverridePropertyName("SignIn") + .WithMessage("Must authenticate with MyAnimeList"); + + RuleFor(c => c.ListStatus).Custom((status, context) => + { + if (!Enum.IsDefined(typeof(MyAnimeListStatus), status)) + { + context.AddFailure($"Invalid status: {status}"); + } + }); + } + } + + public class MyAnimeListSettings : IImportListSettings + { + public string BaseUrl { get; set; } + + protected AbstractValidator Validator => new MalSettingsValidator(); + + public MyAnimeListSettings() + { + BaseUrl = "https://api.myanimelist.net/v2"; + } + + [FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")] + public int ListStatus { get; set; } + + [FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessToken { get; set; } + + [FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RefreshToken { get; set; } + + [FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public DateTime Expires { get; set; } + + [FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)] + public string SignIn { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs new file mode 100644 index 000000000..b08c9e41f --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/MyAnimeList/MyAnimeListStatus.cs @@ -0,0 +1,25 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.MyAnimeList +{ + public enum MyAnimeListStatus + { + [FieldOption(label: "All")] + All = 0, + + [FieldOption(label: "Watching")] + Watching = 1, + + [FieldOption(label: "Completed")] + Completed = 2, + + [FieldOption(label: "On Hold")] + OnHold = 3, + + [FieldOption(label: "Dropped")] + Dropped = 4, + + [FieldOption(label: "Plan to Watch")] + PlanToWatch = 5 + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs index 97420fb65..51b0a75cf 100644 --- a/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/ReleaseSearchService.cs @@ -522,7 +522,7 @@ namespace NzbDrone.Core.IndexerSearch var reports = batch.SelectMany(x => x).ToList(); - _logger.Debug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count); + _logger.ProgressDebug("Total of {0} reports were found for {1} from {2} indexers", reports.Count, criteriaBase, indexers.Count); // Update the last search time for all episodes if at least 1 indexer was searched. if (indexers.Any()) diff --git a/src/NzbDrone.Core/Indexers/RssSyncCommand.cs b/src/NzbDrone.Core/Indexers/RssSyncCommand.cs index 4722b32f2..ea9c6fd8a 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncCommand.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncCommand.cs @@ -5,7 +5,6 @@ namespace NzbDrone.Core.Indexers public class RssSyncCommand : Command { public override bool SendUpdatesToClient => true; - public override bool IsLongRunning => true; } } diff --git a/src/NzbDrone.Core/Localization/Core/ca.json b/src/NzbDrone.Core/Localization/Core/ca.json index 0bba6b2a8..4deede861 100644 --- a/src/NzbDrone.Core/Localization/Core/ca.json +++ b/src/NzbDrone.Core/Localization/Core/ca.json @@ -164,7 +164,7 @@ "AirsTbaOn": "Pendent el seu anunci a {networkLabel}", "AllFiles": "Tots els fitxers", "AllSeriesAreHiddenByTheAppliedFilter": "Tots els resultats estan ocults pel filtre aplicat", - "AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {0}", + "AllSeriesInRootFolderHaveBeenImported": "S'han importat totes les sèries de {path}", "AlreadyInYourLibrary": "Ja a la vostra biblioteca", "AlternateTitles": "Títols alternatius", "AnalyseVideoFilesHelpText": "Extraieu informació de vídeo com ara la resolució, el temps d'execució i la informació del còdec dels fitxers. Això requereix que {appName} llegeixi parts del fitxer que poden provocar una activitat elevada al disc o a la xarxa durant les exploracions.", @@ -183,7 +183,7 @@ "DeleteRootFolder": "Suprimeix la carpeta arrel", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es pot comunicar amb {downloadClientName}. {errorMessage}", "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de baixada no disponibles a causa d'errors: {downloadClientNames}", - "DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {path}. No s'hauria de baixar a una carpeta arrel.", + "DownloadClientRootFolderHealthCheckMessage": "El client de baixada {downloadClientName} col·loca les baixades a la carpeta arrel {rootFolderPath}. No s'hauria de baixar a una carpeta arrel.", "DownloadClientSortingHealthCheckMessage": "El client de baixada {downloadClientName} té l'ordenació {sortingMode} activada per a la categoria de {appName}. Hauríeu de desactivar l'ordenació al vostre client de descàrrega per a evitar problemes d'importació.", "HiddenClickToShow": "Amagat, feu clic per a mostrar", "ImportUsingScript": "Importa amb script", @@ -213,7 +213,7 @@ "FailedToFetchUpdates": "No s'han pogut obtenir les actualitzacions", "False": "Fals", "Implementation": "Implementació", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Falten diverses carpetes arrel per a les llistes d'importació: {rootFolderInfo}", "ImportListRootFolderMissingRootHealthCheckMessage": "Falta la carpeta arrel per a les llistes d'importació: {rootFolderInfo}", "IndexerRssNoIndexersEnabledHealthCheckMessage": "No hi ha indexadors disponibles amb la sincronització RSS activada, {appName} no capturarà els nous llançaments automàticament", "ImportListStatusAllUnavailableHealthCheckMessage": "Totes les llistes no estan disponibles a causa d'errors", @@ -261,7 +261,7 @@ "AuthenticationMethodHelpText": "Es requereix nom d'usuari i contrasenya per a accedir a {appName}", "AutoRedownloadFailedHelpText": "Cerca i intenta baixar automàticament una versió diferent", "AutoTaggingNegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.", - "AutoTaggingRequiredHelpText": "Aquesta condició {implementationName} ha de coincidir perquè s'apliqui la regla d'etiquetatge automàtic. En cas contrari, una única coincidència {implementationName} és suficient.", + "AutoTaggingRequiredHelpText": "La condició {implementationName} ha de coincidir perquè s'apliqui el format personalitzat. En cas contrari, n'hi ha prou amb una única coincidència de {implementationName}.", "BlocklistLoadError": "No es pot carregar la llista de bloqueig", "BlocklistRelease": "Publicació de la llista de bloqueig", "BranchUpdateMechanism": "Branca utilitzada pel mecanisme d'actualització extern", @@ -301,7 +301,7 @@ "Clone": "Clona", "CloneProfile": "Clona el perfil", "CompletedDownloadHandling": "Gestió de descàrregues completades", - "CountSeriesSelected": "{selectedCount} sèries seleccionades", + "CountSeriesSelected": "{count} sèries seleccionades", "InteractiveImportLoadError": "No es poden carregar els elements d'importació manual", "ChownGroupHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega utilitza el mateix grup que {appName}.", "ChmodFolderHelpTextWarning": "Això només funciona si l'usuari que executa {appName} és el propietari del fitxer. És millor assegurar-se que el client de descàrrega estableixi correctament els permisos.", @@ -451,7 +451,7 @@ "DeleteEpisodesFilesHelpText": "Suprimeix els fitxers de l'episodi i la carpeta de la sèrie", "DeleteRemotePathMapping": "Editeu el mapa de camins remots", "DefaultNotFoundMessage": "Deu estar perdut, no hi ha res a veure aquí.", - "DelayMinutes": "{delay} minuts", + "DelayMinutes": "{delay} Minuts", "DelayProfile": "Perfil de retard", "DeleteImportListExclusionMessageText": "Esteu segur que voleu suprimir aquesta exclusió de la llista d'importació?", "DeleteReleaseProfile": "Suprimeix el perfil de llançament", @@ -665,5 +665,51 @@ "NotificationsPushoverSettingsRetry": "Torna-ho a provar", "NotificationsSettingsWebhookMethod": "Mètode", "Other": "Altres", - "Monitor": "Monitora" + "Monitor": "Monitora", + "AutoTaggingSpecificationOriginalLanguage": "Llenguatge", + "AutoTaggingSpecificationQualityProfile": "Perfil de Qualitat", + "AutoTaggingSpecificationRootFolder": "Carpeta arrel", + "AddDelayProfileError": "No s'ha pogut afegir un perfil realentit, torna-ho a probar", + "AutoTaggingSpecificationSeriesType": "Tipus de Sèries", + "AutoTaggingSpecificationStatus": "Estat", + "BlocklistAndSearch": "Llista de bloqueig i cerca", + "BlocklistAndSearchHint": "Comença una cerca per reemplaçar després d'haver bloquejat", + "DownloadClientAriaSettingsDirectoryHelpText": "Ubicació opcional per a les baixades, deixeu-lo en blanc per utilitzar la ubicació predeterminada d'Aria2", + "Directory": "Directori", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Afegeix un prefix a l'url json del Deluge, vegeu {url}", + "Destination": "Destinació", + "Umask": "UMask", + "ConnectionSettingsUrlBaseHelpText": "Afegeix un prefix a l'URL {connectionName}, com ara {url}", + "DoNotBlocklist": "No afegiu a la llista de bloqueig", + "DoNotBlocklistHint": "Elimina sense afegir a la llista de bloqueig", + "Donate": "Dona", + "DownloadClientDelugeTorrentStateError": "Deluge està informant d'un error", + "NegateHelpText": "Si està marcat, el format personalitzat no s'aplicarà si la condició {implementationName} coincideix.", + "TorrentDelayTime": "Retard del torrent: {torrentDelay}", + "CustomFormatsSpecificationRegularExpression": "Expressió regular", + "RemoveFromDownloadClient": "Elimina del client de baixada", + "StartupDirectory": "Directori d'inici", + "ClickToChangeIndexerFlags": "Feu clic per canviar els indicadors de l'indexador", + "ImportListsSettingsSummary": "Importa des d'una altra instància {appName} o llistes de Trakt i gestiona les exclusions de llistes", + "DeleteSpecificationHelpText": "Esteu segur que voleu suprimir l'especificació '{name}'?", + "DeleteSpecification": "Esborra especificació", + "UsenetDelayTime": "Retard d'Usenet: {usenetDelay}", + "DownloadClientDelugeSettingsDirectory": "Directori de baixada", + "DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge", + "RetryingDownloadOn": "S'està retardant la baixada fins al {date} a les {time}", + "ListWillRefreshEveryInterval": "La llista s'actualitzarà cada {refreshInterval}", + "BlocklistAndSearchMultipleHint": "Comença una cerca per reemplaçar després d'haver bloquejat", + "BlocklistMultipleOnlyHint": "Afegeix a la llista de bloqueig sense cercar substituts", + "BlocklistOnly": "Sols afegir a la llista de bloqueig", + "BlocklistOnlyHint": "Afegir a la llista de bloqueig sense cercar substituts", + "ChangeCategory": "Canvia categoria", + "ChangeCategoryHint": "Canvia la baixada a la \"Categoria post-importació\" des del client de descàrrega", + "ChangeCategoryMultipleHint": "Canvia les baixades a la \"Categoria post-importació\" des del client de descàrrega", + "BlocklistReleaseHelpText": "Impedeix que {appName} baixi aquesta versió mitjançant RSS o cerca automàtica", + "MinutesSixty": "60 minuts: {sixty}", + "CustomFilter": "Filtres personalitzats", + "CustomFormatsSpecificationRegularExpressionHelpText": "El format personalitzat RegEx no distingeix entre majúscules i minúscules", + "CustomFormatsSpecificationFlag": "Bandera" } diff --git a/src/NzbDrone.Core/Localization/Core/cs.json b/src/NzbDrone.Core/Localization/Core/cs.json index 352a2cfd9..0f168f6d7 100644 --- a/src/NzbDrone.Core/Localization/Core/cs.json +++ b/src/NzbDrone.Core/Localization/Core/cs.json @@ -30,7 +30,7 @@ "CancelProcessing": "Zrušit zpracování", "CheckDownloadClientForDetails": "zkontrolujte klienta pro stahování pro více informací", "ChmodFolderHelpText": "Octal, aplikováno během importu / přejmenování na mediální složky a soubory (bez provádění bitů)", - "ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", + "ChmodFolderHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", "ChooseAnotherFolder": "Vyberte jinou složku", "ChownGroup": "Skupina chown", "ConnectSettings": "Nastavení připojení", @@ -67,7 +67,7 @@ "CalendarLoadError": "Nelze načíst kalendář", "CertificateValidationHelpText": "Změňte přísnost ověřování certifikátů HTTPS. Neměňte, pokud nerozumíte rizikům.", "ChownGroupHelpText": "Název skupiny nebo gid. Použijte gid pro vzdálené systémy souborů.", - "ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil sonarr, je vlastníkem souboru. Je lepší zajistit, aby klient pro stahování správně nastavil oprávnění.", + "ChownGroupHelpTextWarning": "Toto funguje pouze v případě, že uživatel, který spustil {appName}, je vlastníkem souboru. Je lepší zajistit, aby klient stahování používal stejnou skupinu jako {appName}.", "ClientPriority": "Priorita klienta", "Clone": "Klonovat", "CloneIndexer": "Klonovat indexátor", @@ -319,5 +319,5 @@ "EditSelectedImportLists": "Upravit vybrané seznamy k importu", "FormatDateTime": "{formattedDate} {formattedTime}", "AddRootFolderError": "Nepodařilo se přidat kořenový adresář", - "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {1}." + "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Klient stahování {downloadClientName} je nastaven na odstranění dokončených stahování. To může vést k tomu, že stahování budou z klienta odstraněna dříve, než je bude moci importovat {appName}." } diff --git a/src/NzbDrone.Core/Localization/Core/da.json b/src/NzbDrone.Core/Localization/Core/da.json index 66f4c6531..740402a20 100644 --- a/src/NzbDrone.Core/Localization/Core/da.json +++ b/src/NzbDrone.Core/Localization/Core/da.json @@ -17,5 +17,22 @@ "AddANewPath": "Tilføj en ny sti", "AddConditionImplementation": "Tilføj betingelse - {implementationName}", "AddConnectionImplementation": "Tilføj forbindelse - {implementationName}", - "AddCustomFilter": "Tilføj tilpasset filter" + "AddCustomFilter": "Tilføj tilpasset filter", + "ApplyChanges": "Anvend ændringer", + "Test": "Afprøv", + "AddImportList": "Tilføj importliste", + "AddExclusion": "Tilføj undtagelse", + "TestAll": "Afprøv alle", + "TestAllClients": "Afprøv alle klienter", + "TestAllLists": "Afprøv alle lister", + "Unknown": "Ukendt", + "AllTitles": "All titler", + "TablePageSize": "Sidestørrelse", + "TestAllIndexers": "Afprøv alle indeks", + "AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}", + "AddIndexerError": "Kunne ikke tilføje en ny indekser. Prøv igen.", + "AddImportListImplementation": "Tilføj importliste - {implementationName}", + "AddRootFolderError": "Kunne ikke tilføje rodmappe", + "Table": "Tabel", + "AddIndexer": "Tilføj indekser" } diff --git a/src/NzbDrone.Core/Localization/Core/de.json b/src/NzbDrone.Core/Localization/Core/de.json index 40ea83ad4..901b52856 100644 --- a/src/NzbDrone.Core/Localization/Core/de.json +++ b/src/NzbDrone.Core/Localization/Core/de.json @@ -6,9 +6,9 @@ "RemoveFailedDownloads": "Entferne fehlgeschlagene Downloads", "ApplyChanges": "Änderungen anwenden", "AutomaticAdd": "Automatisch hinzufügen", - "CountSeasons": "{Anzahl} Staffeln", + "CountSeasons": "{count} Staffeln", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Es ist kein Download-Client verfügbar", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Kommunikation mit {downloadClientName} nicht möglich. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "Der Download-Client {downloadClientName} legt Downloads im Stammordner {rootFolderPath} ab. Sie sollten nicht in einen Stammordner herunterladen.", "DownloadClientSortingHealthCheckMessage": "Im Download-Client {downloadClientName} ist die Sortierung {sortingMode} für die Kategorie von {appName} aktiviert. Sie sollten die Sortierung in Ihrem Download-Client deaktivieren, um Importprobleme zu vermeiden.", "DownloadClientStatusSingleClientHealthCheckMessage": "Download-Clients sind aufgrund von Fehlern nicht verfügbar: {downloadClientNames}", @@ -41,7 +41,7 @@ "SkipFreeSpaceCheck": "Prüfung des freien Speichers überspringen", "AbsoluteEpisodeNumber": "Exakte Folgennummer", "AddConnection": "Verbindung hinzufügen", - "AddAutoTagError": "Der neue automatische Tag konnte nicht hinzugefügt werden, bitte versuche es erneut.", + "AddAutoTagError": "Auto-Tag konnte nicht hinzugefügt werden. Bitte erneut versuchen.", "AddConditionError": "Neue Bedingung konnte nicht hinzugefügt werden, bitte erneut versuchen.", "AddCustomFormat": "Eigenes Format hinzufügen", "AddCustomFormatError": "Neues eigenes Format kann nicht hinzugefügt werden, bitte versuchen Sie es erneut.", @@ -86,7 +86,7 @@ "QuickSearch": "Schnelle Suche", "ReadTheWikiForMoreInformation": "Lesen Sie das Wiki für weitere Informationen", "Real": "Real", - "RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {Pfad}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.", + "RecycleBinUnableToWriteHealthCheckMessage": "Es kann nicht in den konfigurierten Papierkorb-Ordner geschrieben werden: {path}. Stellen Sie sicher, dass dieser Pfad vorhanden ist und vom Benutzer, der {appName} ausführt, beschreibbar ist.", "RecyclingBin": "Papierkorb", "RecyclingBinCleanup": "Papierkorb leeren", "RefreshSeries": "Serie aktualisieren", @@ -146,7 +146,7 @@ "AuthenticationRequiredHelpText": "Ändern, welche anfragen Authentifizierung benötigen. Ändere nichts wenn du dir nicht des Risikos bewusst bist.", "AnalyseVideoFilesHelpText": "Videoinformationen wie Auflösung, Laufzeit und Codec-Informationen aus Dateien extrahieren. Dies erfordert, dass {appName} Teile der Datei liest, was bei Scans zu hoher Festplatten- oder Netzwerkaktivität führen kann.", "AnalyticsEnabledHelpText": "Senden Sie anonyme Nutzungs- und Fehlerinformationen an die Server von {appName}. Dazu gehören Informationen zu Ihrem Browser, welche {appName}-WebUI-Seiten Sie verwenden, Fehlerberichte sowie Betriebssystem- und Laufzeitversion. Wir werden diese Informationen verwenden, um Funktionen und Fehlerbehebungen zu priorisieren.", - "AutoTaggingNegateHelpText": "Wenn diese Option aktiviert ist, wird die automatische Tagging-Regel nicht angewendet, wenn diese {implementationName}-Bedingung zutrifft.", + "AutoTaggingNegateHelpText": "Falls aktiviert wird das eigene Format nicht angewendet solange diese {0} Bedingung zutrifft.", "CopyUsingHardlinksSeriesHelpText": "Mithilfe von Hardlinks kann {appName} Seeding-Torrents in den Serienordner importieren, ohne zusätzlichen Speicherplatz zu beanspruchen oder den gesamten Inhalt der Datei zu kopieren. Hardlinks funktionieren nur, wenn sich Quelle und Ziel auf demselben Volume befinden", "DailyEpisodeTypeFormat": "Datum ({format})", "DefaultDelayProfileSeries": "Dies ist das Standardprofil. Es gilt für alle Serien, die kein explizites Profil haben.", @@ -157,7 +157,6 @@ "VisitTheWikiForMoreDetails": "Besuchen Sie das Wiki für weitere Details: ", "UpgradeUntilEpisodeHelpText": "Sobald diese Qualität erreicht ist, lädt {appName} keine Episoden mehr herunter", "SslCertPasswordHelpText": "Passwort für die PFX-Datei", - "ShowMonitoredHelpText": "", "SeriesEditRootFolderHelpText": "Durch das Verschieben von Serien in denselben Stammordner können Serienordner umbenannt werden, um sie an den aktualisierten Titel oder das Benennungsformat anzupassen", "SelectLanguages": "Sprache auswählen", "SelectEpisodesModalTitle": "{modalTitle} – Episode(n) auswählen", @@ -172,7 +171,7 @@ "BackupIntervalHelpText": "Intervall zwischen automatischen Sicherungen", "BuiltIn": "Eingebaut", "ChangeFileDate": "Ändern Sie das Dateidatum", - "CustomFormatsLoadError": "Benutzerdefinierte Formate können nicht geladen werden", + "CustomFormatsLoadError": "Eigene Formate konnten nicht geladen werden", "DeleteQualityProfileMessageText": "Sind Sie sicher, dass Sie das Qualitätsprofil „{name}“ löschen möchten?", "DeletedReasonUpgrade": "Die Datei wurde gelöscht, um ein Upgrade zu importieren", "DeleteEpisodesFiles": "{episodeFileCount} Episodendateien löschen", @@ -203,10 +202,10 @@ "AuthBasic": "Basis (Browser-Popup)", "AuthForm": "Formulare (Anmeldeseite)", "Authentication": "Authentifizierung", - "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich.", + "AuthenticationMethodHelpText": "Für den Zugriff auf {appName} sind Benutzername und Passwort erforderlich", "Automatic": "Automatisch", "AutomaticSearch": "Automatische Suche", - "AutoTaggingRequiredHelpText": "Diese {implementationName}-Bedingung muss zutreffen, damit die automatische Tagging-Regel angewendet wird. Andernfalls reicht eine einzelne {implementationName}-Übereinstimmung aus.", + "AutoTaggingRequiredHelpText": "Diese {0} Bedingungen müssen erfüllt sein, damit das eigene Format zutrifft. Ansonsten reicht ein einzelner {1} Treffer.", "BackupRetentionHelpText": "Automatische Backups, die älter als der Aufbewahrungszeitraum sind, werden automatisch bereinigt", "BindAddressHelpText": "Gültige IP-Adresse, localhost oder „*“ für alle Schnittstellen", "BackupsLoadError": "Sicherrungen können nicht geladen werden", @@ -242,7 +241,6 @@ "ApplyTagsHelpTextHowToApplyImportLists": "So wenden Sie Tags auf die ausgewählten Importlisten an", "ApplyTagsHelpTextHowToApplyDownloadClients": "So wenden Sie Tags auf die ausgewählten Download-Clients an", "ApplyTagsHelpTextHowToApplyIndexers": "So wenden Sie Tags auf die ausgewählten Indexer an", - "Retention": "", "RestrictionsLoadError": "Einschränkungen können nicht geladen werden", "SslCertPath": "SSL-Zertifikatpfad", "TheTvdb": "TheTVDB", @@ -282,23 +280,23 @@ "Custom": "Benutzerdefiniert", "CustomFilters": "Benutzerdefinierte Filter", "CustomFormat": "Benutzerdefiniertes Format", - "CustomFormats": "Benutzerdefinierte Formate", - "CustomFormatsSettingsSummary": "Benutzerdefinierte Formate und Einstellungen", + "CustomFormats": "Eigene Formate", + "CustomFormatsSettingsSummary": "Eigene Formate und Einstellungen", "DailyEpisodeFormat": "Tägliches Episodenformat", "Database": "Datenbank", "Dates": "Termine", "Day": "Tag", "Default": "Standard", "DefaultCase": "Standardfall", - "DefaultNameCopiedProfile": "{Name} – Kopieren", - "DefaultNameCopiedSpecification": "{Name} – Kopieren", + "DefaultNameCopiedProfile": "{name} – Kopieren", + "DefaultNameCopiedSpecification": "{name} – Kopieren", "DefaultNotFoundMessage": "Sie müssen verloren sein, hier gibt es nichts zu sehen.", "DelayMinutes": "{delay} Minuten", "DelayProfile": "Verzögerungsprofil", "DelayProfileProtocol": "Protokoll: {preferredProtocol}", "DelayProfiles": "Verzögerungsprofile", "DelayProfilesLoadError": "Verzögerungsprofile können nicht geladen werden", - "DelayingDownloadUntil": "Download wird bis zum {Datum} um {Uhrzeit} verzögert", + "DelayingDownloadUntil": "Download wird bis zum {date} um {time} verzögert", "DeleteAutoTag": "Auto-Tag löschen", "DeleteAutoTagHelpText": "Sind Sie sicher, dass Sie das automatische Tag „{name}“ löschen möchten?", "DeleteBackup": "Sicherung löschen", @@ -313,8 +311,6 @@ "DeleteEpisodeFromDisk": "Episode von der Festplatte löschen", "DeleteEpisodesFilesHelpText": "Löschen Sie die Episodendateien und den Serienordner", "DeleteImportList": "Importliste löschen", - "DeleteImportListExclusion": "", - "DeleteImportListExclusionMessageText": "", "DeleteIndexerMessageText": "Sind Sie sicher, dass Sie den Indexer „{name}“ löschen möchten?", "DeleteQualityProfile": "Qualitätsprofil löschen", "DeleteReleaseProfile": "Release-Profil löschen", @@ -342,7 +338,6 @@ "DoneEditingGroups": "Bearbeiten der Gruppen abgeschlossen", "DotNetVersion": ".NET", "Download": "Herunterladen", - "DownloadClient": "", "DownloadClientDelugeSettingsUrlBaseHelpText": "Fügt der Deluge-JSON-URL ein Präfix hinzu, siehe {url}", "DownloadClientDelugeTorrentStateError": "Deluge meldet einen Fehler", "DownloadClientDelugeValidationLabelPluginFailure": "Konfiguration des Labels fehlgeschlagen", @@ -355,7 +350,7 @@ "DownloadClientDownloadStationValidationFolderMissing": "Ordner existiert nicht", "DownloadClientDownloadStationValidationFolderMissingDetail": "Der Ordner „{downloadDir}“ existiert nicht, er muss manuell im freigegebenen Ordner „{sharedFolder}“ erstellt werden.", "DownloadClientDownloadStationValidationNoDefaultDestination": "Kein Standardziel", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {Benutzername} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Sie müssen sich bei Ihrer Diskstation als {username} anmelden und sie manuell in den DownloadStation-Einstellungen unter BT/HTTP/FTP/NZB -> Standort einrichten.", "DownloadClientDownloadStationValidationSharedFolderMissing": "Der freigegebene Ordner existiert nicht", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Die Diskstation verfügt nicht über einen freigegebenen Ordner mit dem Namen „{sharedFolder}“. Sind Sie sicher, dass Sie ihn richtig angegeben haben?", "DownloadClientFloodSettingsAdditionalTags": "Zusätzliche Tags", @@ -377,7 +372,7 @@ "DownloadClientFreeboxSettingsAppTokenHelpText": "App-Token, das beim Erstellen des Zugriffs auf die Freebox-API abgerufen wird (z. B. „app_token“)", "DownloadClientFreeboxSettingsHostHelpText": "Hostname oder Host-IP-Adresse der Freebox, standardmäßig „{url}“ (funktioniert nur im selben Netzwerk)", "DownloadClientFreeboxSettingsPortHelpText": "Port, der für den Zugriff auf die Freebox-Schnittstelle verwendet wird, standardmäßig ist „{port}“", - "DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {ExceptionMessage})", + "DownloadClientFreeboxUnableToReachFreebox": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellungen „Host“, „Port“ oder „SSL verwenden“. (Fehler: {exceptionMessage})", "DownloadClientFreeboxUnableToReachFreeboxApi": "Die Freebox-API kann nicht erreicht werden. Überprüfen Sie die Einstellung „API-URL“ für Basis-URL und Version.", "DownloadClientNzbVortexMultipleFilesMessage": "Der Download enthält mehrere Dateien und befindet sich nicht in einem Jobordner: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Diese Option erfordert mindestens NzbGet Version 16.0", @@ -451,7 +446,7 @@ "Restore": "Wiederherstellen", "RestartRequiredWindowsService": "Je nachdem, welcher Benutzer den {appName}-Dienst ausführt, müssen Sie {appName} möglicherweise einmal als Administrator neu starten, bevor der Dienst automatisch gestartet wird.", "RestartSonarr": "{appName} neu starten", - "RetryingDownloadOn": "Erneuter Downloadversuch am {Datum} um {Uhrzeit}", + "RetryingDownloadOn": "Erneuter Downloadversuch am {date} um {time}", "SceneInfo": "Szeneninfo", "Scene": "Szene", "SaveSettings": "Einstellungen speichern", @@ -483,7 +478,6 @@ "Settings": "Einstellungen", "SetTags": "Tags festlegen", "SetPermissionsLinuxHelpTextWarning": "Wenn Sie nicht sicher sind, was diese Einstellungen bewirken, ändern Sie sie nicht.", - "ShowMonitored": "", "ShowEpisodeInformation": "Episodeninformationen anzeigen", "ShowAdvanced": "Erweitert anzeigen", "Space": "Platz", @@ -492,8 +486,6 @@ "StartImport": "Import starten", "StartProcessing": "Verarbeitung starten", "Tasks": "Aufgaben", - "TagIsNotUsedAndCanBeDeleted": "", - "TagDetails": "", "ThemeHelpText": "Ändern Sie das Benutzeroberflächen-Design der Anwendung. Das „Auto“-Design verwendet Ihr Betriebssystemdesign, um den Hell- oder Dunkelmodus festzulegen. Inspiriert vom Theme.Park", "Theme": "Design", "TestAllLists": "Prüfe alle Listen", @@ -501,13 +493,11 @@ "TimeLeft": "Zeit übrig", "Title": "Titel", "ToggleMonitoredToUnmonitored": "Überwacht, klicken Sie, um die Überwachung aufzuheben", - "ToggleMonitoredSeriesUnmonitored ": "", "TorrentBlackholeSaveMagnetFiles": "Speicher Magnetdateien", "Total": "Gesamt", "TorrentsDisabled": "Torrents deaktiviert", "Torrents": "Torrents", "TvdbIdExcludeHelpText": "Die TVDB-ID der auszuschließenden Serie", - "Trace": "", "UiSettingsLoadError": "Die Benutzeroberflächen Einstellungen können nicht geladen werden", "Umask750Description": "{octal} – Besitzer schreibt, Gruppe liest", "Umask": "Umask", @@ -519,7 +509,6 @@ "Unavailable": "Nicht verfügbar", "UnselectAll": "Alle abwählen", "UnsavedChanges": "Nicht gespeicherte Änderungen", - "Unmonitored": "", "UpdateAutomaticallyHelpText": "Updates automatisch herunterladen und installieren. Sie können weiterhin über System: Updates installieren", "UpdateAvailableHealthCheckMessage": "Neues Update ist verfügbar", "UpdateMechanismHelpText": "Verwenden Sie den integrierten Updater von {appName} oder ein Skript", @@ -535,9 +524,8 @@ "Username": "Nutzername", "UsenetDelayTime": "Usenet-Verzögerung: {usenetDelay}", "UsenetDelayHelpText": "Verzögerung in Minuten, bevor Sie eine Veröffentlichung aus dem Usenet erhalten", - "VideoDynamicRange": "", "VideoCodec": "Video-Codec", - "VersionNumber": "Version {Version}", + "VersionNumber": "Version {version}", "Version": "Version", "WantMoreControlAddACustomFormat": "Möchten Sie mehr Kontrolle darüber haben, welche Downloads bevorzugt werden? Fügen Sie ein [benutzerdefiniertes Format] hinzu (/settings/customformats)", "WaitingToProcess": "Warten auf Bearbeitung", @@ -552,7 +540,7 @@ "ApplyTagsHelpTextAdd": "Hinzufügen: Fügen Sie die Tags der vorhandenen Tag-Liste hinzu", "ApplyTagsHelpTextRemove": "Entfernen: Die eingegebenen Tags entfernen", "ApplyTagsHelpTextReplace": "Ersetzen: Ersetzen Sie die Tags durch die eingegebenen Tags (geben Sie keine Tags ein, um alle Tags zu löschen).", - "Wanted": "› Gesucht", + "Wanted": "Gesucht", "ConnectionLostToBackend": "{appName} hat die Verbindung zum Backend verloren und muss neu geladen werden, um die Funktionalität wiederherzustellen.", "Continuing": "Fortsetzung", "CopyUsingHardlinksHelpTextWarning": "Gelegentlich können Dateisperren das Umbenennen von Dateien verhindern, die geseedet werden. Sie können das Seeding vorübergehend deaktivieren und als Workaround die Umbenennungsfunktion von {appName} verwenden.", @@ -561,9 +549,8 @@ "CountImportListsSelected": "{count} Importliste(n) ausgewählt", "CountIndexersSelected": "{count} Indexer ausgewählt", "CountSelectedFiles": "{selectedCount} ausgewählte Dateien", - "CustomFormatUnknownCondition": "Unknown Custom Format condition '{implementation}'", "CustomFormatUnknownConditionOption": "Unbekannte Option „{key}“ für Bedingung „{implementation}“", - "CustomFormatsSettings": "Benutzerdefinierte Formateinstellungen", + "CustomFormatsSettings": "Einstellungen für eigene Formate", "Daily": "Täglich", "Dash": "Bindestrich", "Debug": "Debuggen", @@ -572,7 +559,6 @@ "DeleteRemotePathMappingMessageText": "Sind Sie sicher, dass Sie diese Remote-Pfadzuordnung löschen möchten?", "DeleteSelectedEpisodeFilesHelpText": "Sind Sie sicher, dass Sie die ausgewählten Episodendateien löschen möchten?", "DeleteSpecificationHelpText": "Sind Sie sicher, dass Sie die Spezifikation „{name}“ löschen möchten?", - "DeleteTag": "", "Donations": "Spenden", "Release": "Veröffentlichung", "RelativePath": "Relativer Pfad", @@ -606,7 +592,6 @@ "RemoveSelectedItems": "Markierte Einträge löschen", "RetentionHelpText": "Nur Usenet: Auf Null setzen, um eine unbegrenzte Aufbewahrung festzulegen", "Standard": "Standard", - "Tags": "", "Usenet": "Usenet", "ConnectionLostReconnect": "{appName} wird versuchen, automatisch eine Verbindung herzustellen, oder Sie können unten auf „Neu laden“ klicken.", "CustomFormatJson": "Benutzerdefiniertes JSON-Format", @@ -620,7 +605,6 @@ "UsenetDisabled": "Usenet deaktiviert", "UrlBase": "URL-Basis", "UpgradeUntilThisQualityIsMetOrExceeded": "Führe ein Upgrade durch, bis diese Qualität erreicht oder überschritten wird", - "UpgradesAllowedHelpText": "", "RemovedSeriesMultipleRemovedHealthCheckMessage": "Die Serien {series} wurden aus TheTVDB entfernt", "RemovedFromTaskQueue": "Aus der Aufgabenwarteschlange entfernt", "SceneNumbering": "Szenennummerierung", @@ -725,7 +709,7 @@ "ClickToChangeSeason": "Klicken Sie hier, um die Staffel zu ändern", "BlackholeFolderHelpText": "Ordner, in dem {appName} die Datei {extension} speichert", "BlackholeWatchFolder": "Überwachter Ordner", - "BlackholeWatchFolderHelpText": "Ordner, aus dem {appName} abgeschlossene Downloads importieren soll", + "BlackholeWatchFolderHelpText": "Der Ordner, aus dem {appName} fertige Downloads importieren soll", "BrowserReloadRequired": "Neuladen des Browsers erforderlich", "CalendarOptions": "Kalenderoptionen", "CancelPendingTask": "Möchten Sie diese ausstehende Aufgabe wirklich abbrechen?", @@ -770,7 +754,6 @@ "UsenetDelay": "Usenet-Verzögerung", "UsenetBlackholeNzbFolder": "NZB-Ordner", "UrlBaseHelpText": "Für die Reverse-Proxy-Unterstützung ist der Standardwert leer", - "UpgradeUntilCustomFormatScoreEpisodeHelpText": "", "TestParsing": "Parsing testen", "Test": "Prüfen", "TestAll": "Alle prüfen", @@ -793,5 +776,16 @@ "Airs": "Wird ausgestrahlt", "AddRootFolderError": "Stammverzeichnis kann nicht hinzugefügt werden", "IconForCutoffUnmet": "Symbol für Schwelle nicht erreicht", - "DownloadClientSettingsAddPaused": "Pausiert hinzufügen" + "DownloadClientSettingsAddPaused": "Pausiert hinzufügen", + "ClickToChangeIndexerFlags": "Klicken, um Indexer-Flags zu ändern", + "BranchUpdate": "Branch, der verwendet werden soll, um {appName} zu updaten", + "BlocklistAndSearch": "Sperrliste und Suche", + "AddDelayProfileError": "Verzögerungsprofil konnte nicht hinzugefügt werden. Bitte erneut versuchen.", + "BlocklistAndSearchHint": "Starte Suche nach einer Alternative, falls es der Sperrliste hinzugefügt wurde", + "BlocklistAndSearchMultipleHint": "Starte Suchen nach einer Alternative, falls es der Sperrliste hinzugefügt wurde", + "BlocklistMultipleOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternativen zu suchen", + "BlocklistOnly": "Nur der Sperrliste hinzufügen", + "BlocklistOnlyHint": "Der Sperrliste hinzufügen, ohne nach Alternative zu suchen", + "BlocklistReleaseHelpText": "Dieses Release für erneuten Download durch {appName} via RSS oder automatische Suche sperren", + "ChangeCategory": "Kategorie wechseln" } diff --git a/src/NzbDrone.Core/Localization/Core/el.json b/src/NzbDrone.Core/Localization/Core/el.json index 4be715996..46a7e5880 100644 --- a/src/NzbDrone.Core/Localization/Core/el.json +++ b/src/NzbDrone.Core/Localization/Core/el.json @@ -15,8 +15,8 @@ "RemoveSelectedItemsQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε {selectedCount} αντικείμενα από την ουρά;", "CloneCondition": "Κλωνοποίηση συνθήκης", "RemoveSelectedItemQueueMessageText": "Είστε σίγουροι πως θέλετε να διαγράψετε 1 αντικείμενο από την ουρά;", - "AddConditionImplementation": "Προσθήκη", + "AddConditionImplementation": "Προσθήκη - {implementationName}", "AppUpdated": "{appName} Ενημερώθηκε", "AutoAdd": "Προσθήκη", - "AddConnectionImplementation": "Προσθήκη" + "AddConnectionImplementation": "Προσθήκη - {implementationName}" } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 40cc2f581..f6b312928 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -136,6 +136,7 @@ "AutoTaggingSpecificationRootFolder": "Root Folder", "AutoTaggingSpecificationSeriesType": "Series Type", "AutoTaggingSpecificationStatus": "Status", + "AutoTaggingSpecificationTag": "Tag", "Automatic": "Automatic", "AutomaticAdd": "Automatic Add", "AutomaticSearch": "Automatic Search", @@ -218,6 +219,7 @@ "ClickToChangeLanguage": "Click to change language", "ClickToChangeQuality": "Click to change quality", "ClickToChangeReleaseGroup": "Click to change release group", + "ClickToChangeReleaseType": "Click to change release type", "ClickToChangeSeason": "Click to change season", "ClickToChangeSeries": "Click to change series", "ClientPriority": "Client Priority", @@ -278,6 +280,7 @@ "CustomFormats": "Custom Formats", "CustomFormatsLoadError": "Unable to load Custom Formats", "CustomFormatsSettings": "Custom Formats Settings", + "CustomFormatsSettingsTriggerInfo": "A Custom Format will be applied to a release or file when it matches at least one of each of the different condition types chosen.", "CustomFormatsSettingsSummary": "Custom Formats and Settings", "CustomFormatsSpecificationFlag": "Flag", "CustomFormatsSpecificationLanguage": "Language", @@ -378,7 +381,7 @@ "DeleteTagMessageText": "Are you sure you want to delete the tag '{label}'?", "Deleted": "Deleted", "DeletedReasonEpisodeMissingFromDisk": "{appName} was unable to find the file on disk so the file was unlinked from the episode in the database", - "DeletedReasonManual": "File was deleted by via UI", + "DeletedReasonManual": "File was deleted using {appName}, either manually or by another tool through the API", "DeletedReasonUpgrade": "File was deleted to import an upgrade", "DeletedSeriesDescription": "Series was deleted from TheTVDB", "Destination": "Destination", @@ -413,6 +416,10 @@ "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} was unable to add the label to {clientName}.", "DownloadClientDelugeValidationLabelPluginInactive": "Label plugin not activated", "DownloadClientDelugeValidationLabelPluginInactiveDetail": "You must have the Label plugin enabled in {clientName} to use categories.", + "DownloadClientDelugeSettingsDirectory": "Download Directory", + "DownloadClientDelugeSettingsDirectoryHelpText": "Optional location to put downloads in, leave blank to use the default Deluge location", + "DownloadClientDelugeSettingsDirectoryCompleted": "Move When Completed Directory", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Optional location to move completed downloads to, leave blank to use the default Deluge location", "DownloadClientDownloadStationProviderMessage": "{appName} is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account", "DownloadClientDownloadStationSettingsDirectoryHelpText": "Optional shared folder to put downloads into, leave blank to use the default Download Station location", "DownloadClientDownloadStationValidationApiVersion": "Download Station API version not supported, should be at least {requiredVersion}. It supports from {minVersion} to {maxVersion}", @@ -628,8 +635,10 @@ "EpisodeNaming": "Episode Naming", "EpisodeNumbers": "Episode Number(s)", "EpisodeProgress": "Episode Progress", + "EpisodeRequested": "Episode Requested", "EpisodeSearchResultsLoadError": "Unable to load results for this episode search. Try again later", "EpisodeTitle": "Episode Title", + "EpisodeTitleFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Episode Title:30}`) or the beginning (e.g. `{Episode Title:-30}`) are both supported. Episode titles will be automatically truncated to file system limitations if necessary.", "EpisodeTitleRequired": "Episode Title Required", "EpisodeTitleRequiredHelpText": "Prevent importing for up to 48 hours if the episode title is in the naming format and the episode title is TBA", "Episodes": "Episodes", @@ -833,6 +842,9 @@ "ImportListsImdbSettingsListId": "List ID", "ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)", "ImportListsLoadError": "Unable to load Import Lists", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authenticate with MyAnimeList", + "ImportListsMyAnimeListSettingsListStatus": "List Status", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Type of list you want to import from, set to 'All' for all lists", "ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv", "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", "ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS", @@ -1398,6 +1410,8 @@ "NotificationsTelegramSettingsBotToken": "Bot Token", "NotificationsTelegramSettingsChatId": "Chat ID", "NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages", + "NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications", "NotificationsTelegramSettingsSendSilently": "Send Silently", "NotificationsTelegramSettingsSendSilentlyHelpText": "Sends the message silently. Users will receive a notification with no sound", "NotificationsTelegramSettingsTopicId": "Topic ID", @@ -1581,11 +1595,12 @@ "RelativePath": "Relative Path", "Release": "Release", "ReleaseGroup": "Release Group", + "ReleaseGroupFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Release Group:30}`) or the beginning (e.g. `{Release Group:-30}`) are both supported.`).", "ReleaseGroups": "Release Groups", "ReleaseHash": "Release Hash", "ReleaseProfile": "Release Profile", "ReleaseProfileIndexerHelpText": "Specify what indexer the profile applies to", - "ReleaseProfileIndexerHelpTextWarning": "Using a specific indexer with release profiles can lead to duplicate releases being grabbed", + "ReleaseProfileIndexerHelpTextWarning": "Setting a specific indexer on a release profile will cause this profile to only apply to releases from that indexer.", "ReleaseProfileTagSeriesHelpText": "Release profiles will apply to series with at least one matching tag. Leave blank to apply to all series", "ReleaseProfiles": "Release Profiles", "ReleaseProfilesLoadError": "Unable to load Release Profiles", @@ -1766,11 +1781,13 @@ "SelectLanguages": "Select Languages", "SelectQuality": "Select Quality", "SelectReleaseGroup": "Select Release Group", + "SelectReleaseType": "Select Release Type", "SelectSeason": "Select Season", "SelectSeasonModalTitle": "{modalTitle} - Select Season", "SelectSeries": "Select Series", "SendAnonymousUsageData": "Send Anonymous Usage Data", "Series": "Series", + "SeriesFootNote": "Optionally control truncation to a maximum number of bytes including ellipsis (`...`). Truncating from the end (e.g. `{Series Title:30}`) or the beginning (e.g. `{Series Title:-30}`) are both supported.", "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "Series and episode information is provided by TheTVDB.com. [Please consider supporting them]({url}) .", "SeriesCannotBeFound": "Sorry, that series cannot be found.", "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} episode files", diff --git a/src/NzbDrone.Core/Localization/Core/es.json b/src/NzbDrone.Core/Localization/Core/es.json index fe68fe3e1..ee29b93a7 100644 --- a/src/NzbDrone.Core/Localization/Core/es.json +++ b/src/NzbDrone.Core/Localization/Core/es.json @@ -167,8 +167,8 @@ "AllResultsAreHiddenByTheAppliedFilter": "Todos los resultados están ocultos por el filtro aplicado", "AnalyseVideoFilesHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.", "AnimeEpisodeTypeDescription": "Episodios lanzados usando un número de episodio absoluto", - "ApiKeyValidationHealthCheckMessage": "Actualice su clave de API para que tenga al menos {length} carácteres. Puede hacerlo en los ajustes o en el archivo de configuración", - "AppDataLocationHealthCheckMessage": "No será posible actualizar para prevenir la eliminación de AppData al Actualizar", + "ApiKeyValidationHealthCheckMessage": "Por favor actualiza tu clave API para que tenga de longitud al menos {length} caracteres. Puedes hacerlo en los ajustes o en el archivo de configuración", + "AppDataLocationHealthCheckMessage": "No será posible actualizar para evitar la eliminación de AppData al actualizar", "Scheduled": "Programado", "Season": "Temporada", "Clone": "Clonar", @@ -267,7 +267,7 @@ "DeletedReasonUpgrade": "Se ha borrado el archivo para importar una versión mejorada", "DeleteTagMessageText": "¿Está seguro de querer eliminar la etiqueta '{label}'?", "DisabledForLocalAddresses": "Deshabilitado para Direcciones Locales", - "DeletedReasonManual": "El archivo fue borrado por vía UI", + "DeletedReasonManual": "El archivo fue eliminado usando {appName}, o bien manualmente o por otra herramienta a través de la API", "ClearBlocklist": "Limpiar lista de bloqueos", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirma la nueva contraseña", "MonitorPilotEpisode": "Episodio Piloto", @@ -308,7 +308,7 @@ "CountSeasons": "{count} Temporadas", "BranchUpdate": "Rama a usar para actualizar {appName}", "ChmodFolder": "Carpeta chmod", - "CheckDownloadClientForDetails": "Revisar cliente de descarpa para mas detalles", + "CheckDownloadClientForDetails": "Revisar el cliente de descarga para más detalles", "ChooseAnotherFolder": "Elige otra Carpeta", "ClientPriority": "Prioridad del Cliente", "CloneIndexer": "Clonar Indexer", @@ -324,11 +324,11 @@ "ConnectSettingsSummary": "Notificaciones, conexiones a servidores/reproductores y scripts personalizados", "ConnectSettings": "Conectar Ajustes", "CustomFormatUnknownCondition": "Condición de Formato Personalizado Desconocida '{implementation}'", - "XmlRpcPath": "Ruta XML RPC", - "AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no aplicará si la condición {implementationName} coincide.", + "XmlRpcPath": "Ruta RPC de XML", + "AutoTaggingNegateHelpText": "Si está marcado, la regla de etiquetado automático no se aplicará si esta condición {implementationName} coincide.", "CloneCustomFormat": "Clonar formato personalizado", "Close": "Cerrar", - "AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {0} será suficiente.", + "AutoTaggingRequiredHelpText": "Esta condición {implementationName} debe coincidir para que la regla de etiquetado automático se aplique. De lo contrario una sola coincidencia de {implementationName} será suficiente.", "WeekColumnHeaderHelpText": "Mostrado sobre cada columna cuando la vista activa es semana", "WhyCantIFindMyShow": "Por que no puedo encontrar mi serie?", "WouldYouLikeToRestoreBackup": "Te gustaria restaurar la copia de seguridad '{name}'?", @@ -357,7 +357,7 @@ "ChangeFileDate": "Cambiar fecha de archivo", "CertificateValidationHelpText": "Cambiar como es la validacion de la certificacion estricta de HTTPS. No cambiar a menos que entiendas las consecuencias.", "AddListExclusion": "Agregar Lista de Exclusión", - "AddedDate": "Agregado: {fecha}", + "AddedDate": "Agregado: {date}", "AllSeriesAreHiddenByTheAppliedFilter": "Todos los resultados estan ocultos por el filtro aplicado", "AlternateTitles": "Titulos alternativos", "ChmodFolderHelpText": "Octal, aplicado durante la importación / cambio de nombre a carpetas y archivos multimedia (sin bits de ejecución)", @@ -369,7 +369,7 @@ "AirsTbaOn": "A anunciar en {networkLabel}", "AllFiles": "Todos los archivos", "Any": "Cualquiera", - "AirsTomorrowOn": "Mañana a las {hora} en {networkLabel}", + "AirsTomorrowOn": "Mañana a las {time} en {networkLabel}", "AppUpdatedVersion": "{appName} ha sido actualizado a la versión `{version}`, para obtener los cambios más recientes, necesitará recargar {appName} ", "AddListExclusionSeriesHelpText": "Evitar que las series sean agregadas a {appName} por las listas", "CalendarLegendEpisodeDownloadedTooltip": "El episodio fue descargado y ordenado", @@ -530,20 +530,20 @@ "DeleteEpisodesFilesHelpText": "Eliminar archivos de episodios y directorio de series", "DoNotPrefer": "No preferir", "DoNotUpgradeAutomatically": "No actualizar automáticamente", - "IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, en blanco usa el defecto por el cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores", + "IndexerSettingsSeedRatioHelpText": "El ratio que un torrent debería alcanzar antes de detenerse, vacío usa el predeterminado del cliente de descarga. El ratio debería ser al menos 1.0 y seguir las reglas de los indexadores", "Download": "Descargar", "Donate": "Donar", "DownloadClientDelugeValidationLabelPluginFailure": "Falló la configuración de la etiqueta", "DownloadClientDelugeTorrentStateError": "Deluge está informando de un error", "DownloadClientDownloadStationValidationFolderMissing": "No existe la carpeta", "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Debes iniciar sesión en tu Diskstation como {username} y configurarlo manualmente en los ajustes de DownloadStation en BT/HTTP/FTP/NZB -> Ubicación.", - "DownloadClientFreeboxSettingsAppIdHelpText": "ID de la aplicación cuando se crea acceso a la API de Freebox (i.e. 'app_id')", - "DownloadClientFreeboxSettingsAppToken": "Token de la aplicación", + "DownloadClientFreeboxSettingsAppIdHelpText": "ID de la app dada cuando se crea acceso a la API de Freebox (esto es 'app_id')", + "DownloadClientFreeboxSettingsAppToken": "Token de la app", "DownloadClientFreeboxUnableToReachFreebox": "No es posible acceder a la API de Freebox. Verifica las opciones 'Host', 'Puerto' o 'Usar SSL'. (Error: {exceptionMessage})", "DownloadClientNzbVortexMultipleFilesMessage": "La descarga contiene varios archivos y no está en una carpeta de trabajo: {outputPath}", "DownloadClientNzbgetSettingsAddPausedHelpText": "Esta opción requiere al menos NzbGet versión 16.0", "DownloadClientOptionsLoadError": "No es posible cargar las opciones del cliente de descarga", - "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta tendrá que ser accesible desde XBMC", + "DownloadClientPneumaticSettingsNzbFolderHelpText": "Esta carpeta necesitará ser alcanzable desde XBMC", "DownloadClientPneumaticSettingsNzbFolder": "Carpeta de Nzb", "Docker": "Docker", "DockerUpdater": "Actualiza el contenedor docker para recibir la actualización", @@ -570,17 +570,17 @@ "DotNetVersion": ".NET", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Ningún cliente de descarga disponible", "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "No es posible comunicarse con {downloadClientName}. {errorMessage}", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo a la url json de deluge, ver {url}", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Añade un prefijo al url del json de deluge, vea {url}", "DownloadClientDownloadStationValidationApiVersion": "Versión de la API de la Estación de Descarga no soportada, debería ser al menos {requiredVersion}. Soporte desde {minVersion} hasta {maxVersion}", "DownloadClientDownloadStationValidationFolderMissingDetail": "No existe la carpeta '{downloadDir}', debe ser creada manualmente dentro de la Carpeta Compartida '{sharedFolder}'.", "DownloadClientDownloadStationValidationNoDefaultDestination": "Sin destino predeterminado", "DownloadClientFreeboxNotLoggedIn": "No ha iniciado sesión", - "DownloadClientFreeboxSettingsApiUrl": "URL de la API", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API de Freebox con la versión de la API, p.ej. '{url}', por defecto es '{defaultApiUrl}'", - "DownloadClientFreeboxSettingsAppId": "ID de la aplicación", - "DownloadClientFreeboxSettingsAppTokenHelpText": "App token recuperado cuando se crea el acceso a la API de Freebox (i.e. 'app_token')", - "DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, por defecto es '{port}'", - "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP del Freebox, por defecto es '{url}' (solo funcionará en la misma red)", + "DownloadClientFreeboxSettingsApiUrl": "URL de API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Define la URL base de la API Freebox con la versión de la API, p. ej. '{url}', por defecto a '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppId": "ID de la app", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Token de la app recuperado cuando se crea acceso a la API de Freebox (esto es 'app_token')", + "DownloadClientFreeboxSettingsPortHelpText": "Puerto usado para acceder a la interfaz de Freebox, predeterminado a '{port}'", + "DownloadClientFreeboxSettingsHostHelpText": "Nombre de host o dirección IP de host del Freebox, predeterminado a '{url}' (solo funcionará en la misma red)", "DownloadClientFreeboxUnableToReachFreeboxApi": "No es posible acceder a la API de Freebox. Verifica la configuración 'URL de la API' para la URL base y la versión.", "DownloadClientNzbgetValidationKeepHistoryOverMax": "La opción KeepHistory de NZBGet debería ser menor de 25000", "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "La opción KeepHistory de NzbGet está establecida demasiado alta.", @@ -605,7 +605,7 @@ "EnableHelpText": "Habilitar la creación de un fichero de metadatos para este tipo de metadato", "EnableMediaInfoHelpText": "Extraer información de video como la resolución, el tiempo de ejecución y la información del códec de los archivos. Esto requiere que {appName} lea partes del archivo lo cual puede causar una alta actividad en el disco o en la red durante los escaneos.", "TheLogLevelDefault": "El nivel de registro por defecto es 'Info' y puede ser cambiado en [Opciones generales](opciones/general)", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, por defecto es '.magnet'", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extensión a usar para enlaces magnet, predeterminado a '.magnet'", "DownloadIgnoredEpisodeTooltip": "Descarga de episodio ignorada", "EditDelayProfile": "Editar perfil de retraso", "DownloadClientFloodSettingsUrlBaseHelpText": "Añade un prefijo a la API de Flood, como {url}", @@ -613,13 +613,13 @@ "EditReleaseProfile": "Editar perfil de lanzamiento", "DownloadClientPneumaticSettingsStrmFolder": "Carpeta de Strm", "DownloadClientQbittorrentValidationCategoryAddFailure": "Falló la configuración de categoría", - "DownloadClientRTorrentSettingsUrlPath": "Ruta de la url", + "DownloadClientRTorrentSettingsUrlPath": "Ruta de url", "DownloadClientSabnzbdValidationDevelopVersion": "Versión de desarrollo de Sabnzbd, asumiendo versión 3.0.0 o superior.", "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "Usar 'Verificar antes de descargar' afecta a la habilidad de {appName} de rastrear nuevas descargas. Sabnzbd también recomienda 'Abortar trabajos que no pueden ser completados' en su lugar ya que resulta más efectivo.", - "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName] puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} puede no ser capaz de soportar nuevas características añadidas a SABnzbd cuando se ejecutan versiones de desarrollo.", "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Debe deshabilitar la ordenación por fechas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.", "DownloadClientSabnzbdValidationEnableJobFolders": "Habilitar carpetas de trabajo", - "DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url de {clientName}, como {url}", + "DownloadClientSettingsUrlBaseHelpText": "Añade un prefijo a la url {clientName}, como {url}", "DownloadClientStatusAllClientHealthCheckMessage": "Ningún cliente de descarga está disponible debido a fallos", "DownloadClientValidationGroupMissing": "El grupo no existe", "DownloadClientValidationSslConnectFailure": "No es posible conectarse a través de SSL", @@ -641,7 +641,7 @@ "EnableInteractiveSearchHelpText": "Se usará cuando se utilice la búsqueda interactiva", "DoneEditingGroups": "Terminado de editar grupos", "DownloadClientFloodSettingsAdditionalTags": "Etiquetas adicionales", - "DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades multimedia como etiquetas. Sugerencias a modo de ejemplo.", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Añade propiedades de medios como etiquetas. Los consejos son ejemplos.", "DownloadClientFloodSettingsPostImportTagsHelpText": "Añade etiquetas después de que se importe una descarga.", "DownloadClientFloodSettingsRemovalInfo": "{appName} manejará la eliminación automática de torrents basada en el criterio de sembrado actual en Ajustes -> Indexadores", "DownloadClientFloodSettingsPostImportTags": "Etiquetas tras importación", @@ -649,7 +649,7 @@ "DownloadClientFloodSettingsStartOnAdd": "Inicial al añadir", "DownloadClientFreeboxApiError": "La API de Freebox devolvió el error: {errorDescription}", "DownloadClientFreeboxAuthenticationError": "La autenticación a la API de Freebox falló. El motivo: {errorDescription}", - "DownloadClientPneumaticSettingsStrmFolderHelpText": "Se importarán los archivos .strm en esta carpeta por drone", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Los archivos .strm en esta carpeta será importados por drone", "DownloadClientQbittorrentTorrentStateError": "qBittorrent está informando de un error", "DownloadClientQbittorrentSettingsSequentialOrder": "Orden secuencial", "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent no puede resolver enlaces magnet con DHT deshabilitado", @@ -658,26 +658,26 @@ "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} no intentará importar descargas completadas sin una categoría.", "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "Poner en cola el torrent no está habilitado en los ajustes de su qBittorrent. Habilítelo en qBittorrent o seleccione 'Último' como prioridad.", "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} no podrá efectuar el Manejo de Descargas Completadas según lo configurado. Puede arreglar esto en qBittorrent ('Herramientas -> Opciones...' en el menú) cambiando 'Opciones -> BitTorrent -> Límite de ratio ' de 'Eliminarlas' a 'Pausarlas'", - "DownloadClientRTorrentSettingsAddStopped": "Añadir parados", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "Habilitarlo añadirá los torrents y magnets a rTorrent en un estado parado. Esto puede romper los archivos magnet.", - "DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de rTorrent", + "DownloadClientRTorrentSettingsAddStopped": "Añadir detenido", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "Permite añadir torrents y magnets a rTorrent en estado detenido. Esto puede romper los archivos magnet.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de rTorrent", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "El cliente de descarga {downloadClientName} se establece para eliminar las descargas completadas. Esto puede resultar en descargas siendo eliminadas de tu cliente antes de que {appName} pueda importarlas.", "DownloadClientRootFolderHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en la carpeta raíz {rootFolderPath}. No debería descargar a una carpeta raíz.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Deshabilitar la ordenación por fechas", "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Debe deshabilitar la ordenación de películas para la categoría que {appName} usa para evitar problemas al importar. Vaya a Sabnzbd para arreglarlo.", "DownloadClientSettingsCategorySubFolderHelpText": "Añadir una categoría específica a {appName} evita conflictos con descargas no-{appName} no relacionadas. Usar una categoría es opcional, pero bastante recomendado. Crea un subdirectorio [categoría] en el directorio de salida.", - "DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, déjelo en blanco para usar el predeterminado", + "DownloadClientSettingsDestinationHelpText": "Especifica manualmente el destino de descarga, dejar en blanco para usar el predeterminado", "DownloadClientSettingsInitialState": "Estado inicial", - "DownloadClientSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a {clientName}", + "DownloadClientSettingsInitialStateHelpText": "Estado inicial para torrents añadidos a {clientName}", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión hace más de 14 días", "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent está descargando metadatos", "DownloadClientSettingsPostImportCategoryHelpText": "Categoría para {appName} que se establece después de que se haya importado la descarga. {appName} no eliminará los torrents en esa categoría incluso si finalizó la siembra. Déjelo en blanco para mantener la misma categoría.", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Prioridad a usar cuando se tomen episodios que llevan en emisión dentro de los últimos 14 días", - "DownloadClientSettingsUseSslHelpText": "Usa conexión segura cuando haya una conexión a {clientName}", + "DownloadClientSettingsUseSslHelpText": "Usa una conexión segura cuando haya una conexión a {clientName}", "DownloadClientSortingHealthCheckMessage": "El cliente de descarga {downloadClientName} tiene habilitada la ordenación {sortingMode} para la categoría de {appName}. Debería deshabilitar la ordenación en su cliente de descarga para evitar problemas al importar.", "DownloadClientStatusSingleClientHealthCheckMessage": "Clientes de descarga no disponibles debido a fallos: {downloadClientNames}", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, déjelo en blanco para usar la ubicación predeterminada de Transmission", - "DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p.ej. {url}, por defecto es '{defaultUrl}'", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Transmission", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Añade un prefijo a la url rpc de {clientName}, p. ej. {url}, predeterminado a '{defaultUrl}'", "DownloadClientUTorrentTorrentStateError": "uTorrent está informando de un error", "DownloadClientValidationApiKeyIncorrect": "Clave API incorrecta", "DownloadClientValidationApiKeyRequired": "Clave API requerida", @@ -706,11 +706,11 @@ "DownloadClientSabnzbdValidationUnknownVersion": "Versión desconocida: {rawVersion}", "DownloadClientSettingsAddPaused": "Añadir pausado", "DownloadClientSeriesTagHelpText": "Solo use este cliente de descarga para series con al menos una etiqueta coincidente. Déjelo en blanco para usarlo con todas las series.", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Consulte Opciones -> Interfaz Web -> 'Usar HTTPS en lugar de HTTP' en qBittorrent.", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Usa una conexión segura. Ver en Opciones -> Interfaz web -> 'Usar HTTPS en lugar de HTTP' en qbittorrent.", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} no pudo añadir la etiqueta a qBittorrent.", "DownloadClientQbittorrentValidationCategoryUnsupported": "La categoría no está soportada", "DownloadClientQbittorrentValidationQueueingNotEnabled": "Poner en cola no está habilitado", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, vea {url}. Esto es usualmente RPC2 o [ruta a rTorrent]{url2} cuando se usa rTorrent.", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Ruta al endpoint de XMLRPC, ver {url}. Esto es usualmente RPC2 o [ruta a ruTorrent]{url2} cuando se usa ruTorrent.", "DownloadClientQbittorrentTorrentStatePathError": "No es posible importar. La ruta coincide con el directorio de descarga base del cliente, ¿es posible que 'Mantener carpeta de nivel superior' esté deshabilitado para este torrent o que 'Diseño de contenido de torrent' NO se haya establecido en 'Original' o 'Crear subcarpeta'?", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Deshabilitar la ordenación de películas", "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Deshabilitar ordenación de TV", @@ -733,8 +733,8 @@ "DownloadClientValidationSslConnectFailureDetail": "{appName} no se puede conectar a {clientName} usando SSL. Este problema puede estar relacionado con el ordenador. Por favor, intente configurar tanto {appName} como {clientName} para no usar SSL.", "DownloadFailedEpisodeTooltip": "La descarga del episodio falló", "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Descarga primero las primeras y últimas piezas (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primero las primeras y últimas", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Tenga en cuenta que Torrents forzados no se atiene a las restricciones de sembrado", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Primeras y últimas primero", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "Estado inicial para los torrents añadidos a qBittorrent. Ten en cuenta que Forzar torrents no cumple las restricciones de semilla", "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Descarga en orden secuencial (qBittorrent 4.1.0+)", "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "El Diskstation no tiene una Carpeta Compartida con el nombre '{sharedFolder}', ¿estás seguro que lo has especificado correctamente?", "EnableCompletedDownloadHandlingHelpText": "Importar automáticamente las descargas completas del gestor de descargas", @@ -825,7 +825,7 @@ "Existing": "Existentes", "ExportCustomFormat": "Exportar formato personalizado", "EpisodeFilesLoadError": "No se puede cargar los archivos de episodios", - "EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadCliente}", + "EpisodeGrabbedTooltip": "Episodio capturado desde {indexer} y enviado a {downloadClient}", "EpisodeInfo": "Información del episodio", "EpisodeMissingAbsoluteNumber": "El episodio no tiene un número de episodio absoluto", "EpisodeTitleRequired": "Título del episodio requerido", @@ -873,7 +873,7 @@ "FilterNotInLast": "no en el último", "Group": "Grupo", "ImportListSearchForMissingEpisodes": "Buscar episodios faltantes", - "EnableProfileHelpText": "Señalar para habilitar el perfil de lanzamiento", + "EnableProfileHelpText": "Marcar para habilitar el perfil de lanzamiento", "EnableRssHelpText": "Se usará cuando {appName} busque periódicamente lanzamientos vía Sincronización RSS", "EndedSeriesDescription": "No se esperan episodios o temporadas adicionales", "EpisodeFileDeleted": "Archivo de episodio eliminado", @@ -1005,8 +1005,8 @@ "Forecast": "Previsión", "IndexerDownloadClientHelpText": "Especifica qué cliente de descarga es usado para capturas desde este indexador", "IndexerHDBitsSettingsCodecs": "Códecs", - "IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usan todas las opciones.", - "IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usan todas las opciones.", + "IndexerHDBitsSettingsCodecsHelpText": "Si no se especifica, se usarán todas las opciones.", + "IndexerHDBitsSettingsMediumsHelpText": "Si no se especifica, se usarán todas las opciones.", "IndexerPriority": "Prioridad del indexador", "IconForFinales": "Icono para Finales", "IgnoreDownload": "Ignorar descarga", @@ -1059,7 +1059,7 @@ "ICalTagsSeriesHelpText": "El feed solo contendrá series con al menos una etiqueta coincidente", "IconForCutoffUnmet": "Icono para Umbrales no alcanzados", "IconForCutoffUnmetHelpText": "Mostrar icono para archivos cuando el umbral no haya sido alcanzado", - "EpisodeCount": "Número de episodios", + "EpisodeCount": "Recuento de episodios", "IndexerSettings": "Ajustes de Indexador", "AddDelayProfileError": "No se pudo añadir un nuevo perfil de retraso, inténtelo de nuevo.", "IndexerRssNoIndexersAvailableHealthCheckMessage": "Todos los indexers capaces de RSS están temporalmente desactivados debido a errores recientes con el indexer", @@ -1068,23 +1068,23 @@ "IndexerSearchNoAvailableIndexersHealthCheckMessage": "Todos los indexadores con capacidad de búsqueda no están disponibles temporalmente debido a errores recientes del indexadores", "IndexerSearchNoInteractiveHealthCheckMessage": "No hay indexadores disponibles con Búsqueda Interactiva activada, {appName} no proporcionará ningún resultado de búsquedas interactivas", "PasswordConfirmation": "Confirmación de Contraseña", - "IndexerSettingsAdditionalParameters": "Parámetros Adicionales", + "IndexerSettingsAdditionalParameters": "Parámetros adicionales", "IndexerSettingsAllowZeroSizeHelpText": "Activar esta opción le permitirá utilizar fuentes que no especifiquen el tamaño del lanzamiento, pero tenga cuidado, no se realizarán comprobaciones relacionadas con el tamaño.", "IndexerSettingsAllowZeroSize": "Permitir Tamaño Cero", "StopSelecting": "Detener la Selección", "IndexerSettingsCookie": "Cookie", "IndexerSettingsCategories": "Categorías", "IndexerSettingsMinimumSeedersHelpText": "Número mínimo de semillas necesario.", - "IndexerSettingsSeedRatio": "Proporción de Semillado", + "IndexerSettingsSeedRatio": "Ratio de sembrado", "StartupDirectory": "Directorio de Arranque", "IndexerSettingsAdditionalParametersNyaa": "Parámetros Adicionales", "IndexerSettingsPasskey": "Clave de acceso", "IndexerSettingsSeasonPackSeedTime": "Tiempo de Semillado de los Pack de Temporada", "IndexerSettingsAnimeStandardFormatSearch": "Formato Estándar de Búsqueda de Anime", "IndexerSettingsAnimeStandardFormatSearchHelpText": "Buscar también anime utilizando la numeración estándar", - "IndexerSettingsApiPathHelpText": "Ruta a la api, normalmente {url}", + "IndexerSettingsApiPathHelpText": "Ruta a la API, usualmente {url}", "IndexerSettingsSeasonPackSeedTimeHelpText": "La cantidad de tiempo que un torrent de pack de temporada debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga", - "IndexerSettingsSeedTime": "Tiempo de Semillado", + "IndexerSettingsSeedTime": "Tiempo de sembrado", "IndexerStatusAllUnavailableHealthCheckMessage": "Todos los indexadores no están disponibles debido a errores", "IndexerValidationCloudFlareCaptchaExpired": "El token CAPTCHA de CloudFlare ha caducado, actualícelo.", "NotificationsDiscordSettingsAuthor": "Autor", @@ -1097,12 +1097,12 @@ "IndexerSettingsMinimumSeeders": "Semillas mínimas", "IndexerSettingsRssUrl": "URL de RSS", "IndexerSettingsAnimeCategoriesHelpText": "Lista desplegable, dejar en blanco para desactivar anime", - "IndexerSettingsApiPath": "Ruta de la API", + "IndexerSettingsApiPath": "Ruta de API", "IndexerSettingsCookieHelpText": "Si su sitio requiere una cookie de inicio de sesión para acceder al RSS, tendrá que conseguirla a través de un navegador.", "IndexerSettingsRssUrlHelpText": "Introduzca la URL de un canal RSS compatible con {indexer}", "IndexerStatusUnavailableHealthCheckMessage": "Indexadores no disponibles debido a errores: {indexerNames}", "IndexerHDBitsSettingsMediums": "Medios", - "IndexerSettingsSeedTimeHelpText": "La cantidad de tiempo que un torrent debe ser compartido antes de que se detenga, dejar vacío utiliza el valor por defecto del cliente de descarga", + "IndexerSettingsSeedTimeHelpText": "El tiempo que un torrent debería ser compartido antes de detenerse, vació usa el predeterminado del cliente de descarga", "IndexerValidationCloudFlareCaptchaRequired": "Sitio protegido por CloudFlare CAPTCHA. Se requiere un token CAPTCHA válido.", "NotificationsEmailSettingsUseEncryption": "Usar Cifrado", "LastDuration": "Última Duración", @@ -1177,7 +1177,7 @@ "LogFiles": "Archivos de Registro", "LogLevel": "Nivel de Registro", "LogLevelTraceHelpTextWarning": "El registro de seguimiento sólo debe activarse temporalmente", - "LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. ej. 'episodio.s02e15.bluray.mkv'.", + "LibraryImportTipsQualityInEpisodeFilename": "Asegúrate de que tus archivos incluyen la calidad en sus nombres de archivo. P. ej. `episodio.s02e15.bluray.mkv`", "ListSyncLevelHelpText": "Las series de la biblioteca se gestionarán en función de su selección si se caen o no aparecen en su(s) lista(s)", "LogOnly": "Sólo Registro", "LongDateFormat": "Formato de Fecha Larga", @@ -1335,7 +1335,7 @@ "Monitor": "Monitorizar", "MonitorAllEpisodes": "Todos los episodios", "MonitorAllSeasons": "Todas las temporadas", - "NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeSet}, asegúrate de que tu script maneja esto correctamente", + "NotificationsCustomScriptSettingsProviderMessage": "El test ejecutará el script con el EventType establecido en {eventTypeTest}, asegúrate de que tu script maneja esto correctamente", "NotificationsDiscordSettingsAvatar": "Avatar", "NotificationsDiscordSettingsAvatarHelpText": "Cambia el avatar que es usado para mensajes desde esta integración", "NotificationsAppriseSettingsNotificationType": "Tipo de notificación de Apprise", @@ -1351,7 +1351,7 @@ "NotificationsDiscordSettingsAuthorHelpText": "Sobrescribe el autor incrustado que se muestra para esta notificación. En blanco es el nombre de la instancia", "MonitorNewItems": "Monitorizar nuevos elementos", "MonitoredEpisodesHelpText": "Descargar episodios monitorizados en estas series", - "NegateHelpText": "Si se elige, el formato personalizado no se aplica si coincide la condición {implementationName}.", + "NegateHelpText": "Si se marca, el formato personalizado no se aplica si coincide la condición {implementationName}.", "NotificationsCustomScriptSettingsName": "Script personalizado", "ImportListsSonarrSettingsSyncSeasonMonitoring": "Sincronizar la monitorización de temporada", "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "Sincroniza la monitorización de temporada de la instancia de {appName}, si se habilita 'Monitorizar' será ignorado", @@ -1361,5 +1361,713 @@ "MoreDetails": "Más detalles", "MoreInfo": "Más información", "NoEpisodesInThisSeason": "No hay episodios en esta temporada", - "NoLinks": "No hay enlaces" + "NoLinks": "No hay enlaces", + "OrganizeSelectedSeriesModalAlert": "Consejo: Para previsualizar un renombrado, selecciona \"Cancelar\", entonces selecciona cualquier título de serie y usa este icono:", + "OrganizeSelectedSeriesModalConfirmation": "¿Estás seguro que quieres organizar todos los archivos en las {count} series seleccionadas?", + "Password": "Contraseña", + "Permissions": "Permisos", + "Port": "Puerto", + "RecyclingBinCleanup": "Limpieza de la papelera de reciclaje", + "ReleaseSceneIndicatorSourceMessage": "Los lanzamientos {message} existen con numeración ambigua, no se pudo identificar de forma fiable el episodio.", + "SeriesTitle": "Título de serie", + "ShowEpisodes": "Mostrar episodios", + "ShowBanners": "Mostrar banners", + "ShowSeriesTitleHelpText": "Muestra el título de serie bajo el póster", + "SkipFreeSpaceCheck": "Saltar comprobación de espacio libre", + "OneSeason": "1 temporada", + "OnlyTorrent": "Solo torrent", + "OpenBrowserOnStart": "Abrir navegador al inicio", + "OnlyUsenet": "Solo Usenet", + "OverrideAndAddToDownloadQueue": "Sobrescribe y añade a la cola de descarga", + "Table": "Tabla", + "TagsLoadError": "No se pudo cargar Etiquetas", + "OverviewOptions": "Opciones de vista general", + "Umask775Description": "{octal} - Usuario y grupo escriben, Otros leen", + "PendingChangesStayReview": "Quedarse y revisar cambios", + "PendingDownloadClientUnavailable": "Pendiente - El cliente de descarga no está disponible", + "PostImportCategory": "Categoría de post-importación", + "PreferUsenet": "Preferir usenet", + "PreviousAiringDate": "Emisiones anteriores: {date}", + "Profiles": "Perfiles", + "PrioritySettings": "Prioridad: {priority}", + "Ok": "Ok", + "PrefixedRange": "Rango prefijado", + "Qualities": "Calidades", + "PublishedDate": "Fecha de publicación", + "QualitySettings": "Opciones de calidad", + "QualitySettingsSummary": "Tamaños de calidad y nombrado", + "RecentChanges": "Cambios recientes", + "MountSeriesHealthCheckMessage": "El montaje que contiene una ruta de series se monta en solo lectura: ", + "NotificationsEmailSettingsBccAddress": "Dirección(es) BCC", + "NotificationsEmailSettingsBccAddressHelpText": "Lista separada por coma de destinatarios de e-mail bcc", + "NotificationsEmailSettingsName": "E-mail", + "NotificationsEmailSettingsRecipientAddress": "Dirección(es) de destinatario", + "NotificationsEmbySettingsSendNotificationsHelpText": "Hacer que MediaBrowser envíe notificaciones a los proveedores configurados", + "NotificationsGotifySettingsAppToken": "Token de app", + "NotificationsGotifySettingIncludeSeriesPosterHelpText": "Incluye poster de serie en mensaje", + "NotificationsJoinSettingsDeviceNames": "Nombres de dispositivo", + "NotificationsJoinSettingsDeviceNamesHelpText": "Lista separada por coma de nombres de dispositivo completos o parciales a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.", + "NotificationsJoinSettingsNotificationPriority": "Prioridad de notificación", + "NotificationsNtfySettingsClickUrlHelpText": "Enlace opcional cuando el usuario hace clic en la notificación", + "NotificationsNtfySettingsPasswordHelpText": "Contraseña opcional", + "NotificationsNtfySettingsTagsEmojis": "Etiquetas y emojis de Ntfy", + "NotificationsNtfySettingsServerUrlHelpText": "Deja en blanco para usar el servidor público ({url})", + "NotificationsNtfySettingsTopicsHelpText": "Lista de temas a la que enviar notificaciones", + "NotificationsPushBulletSettingSenderIdHelpText": "La ID del dispositivo desde la que enviar notificaciones, usa device_iden en la URL del dispositivo en pushbullet.com (deja en blanco para enviarla por ti mismo)", + "NotificationsPushBulletSettingsChannelTagsHelpText": "Lista de etiquetas de canal a las que enviar notificaciones", + "NotificationsSettingsUpdateMapPathsFromHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')", + "NotificationsSettingsUpdateMapPathsFrom": "Mapear rutas desde", + "NotificationsTagsSeriesHelpText": "Envía notificaciones solo para series con al menos una etiqueta coincidente", + "NotificationsTraktSettingsRefreshToken": "Refrescar token", + "OnEpisodeFileDelete": "Al borrar un archivo de episodio", + "OnGrab": "Al capturar", + "OneMinute": "1 minuto", + "Or": "o", + "OrganizeSelectedSeriesModalHeader": "Organizar series seleccionadas", + "Original": "Original", + "OriginalLanguage": "Idioma original", + "OverrideGrabNoLanguage": "Al menos un idioma debe ser seleccionado", + "ParseModalHelpTextDetails": "{appName} intentará analizar el título y te mostrará detalles sobre ello", + "Parse": "Analizar", + "Path": "Ruta", + "PortNumber": "Número de puerto", + "PosterSize": "Tamaño de póster", + "Posters": "Pósteres", + "PreviewRename": "Previsualizar renombrado", + "PreferredProtocol": "Protocolo preferido", + "ProcessingFolders": "Procesando carpetas", + "Proper": "Proper", + "ProxyFailedToTestHealthCheckMessage": "Fallo al probar el proxy: {url}", + "ProxyBadRequestHealthCheckMessage": "Fallo al probar el proxy. Código de estado: {statusCode}", + "ProxyType": "Tipo de proxy", + "QualityLimitsSeriesRuntimeHelpText": "Los límites son automáticamente ajustados para las series en tiempo de ejecución y el número de episodios en el archivo.", + "Range": "Rango", + "RecycleBinUnableToWriteHealthCheckMessage": "No se pudo escribir en la carpeta configurada de la papelera de reciclaje: {path}. Asegúrate de que la ruta existe y es modificable por el usuario que ejecuta {appName}", + "RecyclingBinHelpText": "Los archivos irán aquí cuando se borren en lugar de ser borrados permanentemente", + "RelativePath": "Ruta relativa", + "RegularExpressionsCanBeTested": "Las expresiones regulares pueden ser probadas [aquí]({url}).", + "ReleaseGroup": "Grupo de lanzamiento", + "ReleaseGroups": "Grupos de lanzamiento", + "ReleaseProfilesLoadError": "No se pudo cargar los perfiles de lanzamiento", + "RemotePathMappingImportEpisodeFailedHealthCheckMessage": "{appName} falló al importar (un) episodio(s). Comprueba tus registros para más detalles.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero este directorio no parece existir. Posiblemente mapeo de ruta remota perdido.", + "RemoveQueueItem": "Eliminar - {sourceTitle}", + "RemoveFailed": "Fallo al eliminar", + "ResetQualityDefinitions": "Restablecer definiciones de calidad", + "Scene": "Escena", + "RssSyncIntervalHelpText": "Intervalo en minutos. Configurar a cero para deshabilitar (esto detendrá todas las capturas automáticas de lanzamientos)", + "SceneNumberNotVerified": "El número de escena no ha sido verificado aún", + "SearchForAllMissingEpisodes": "Buscar todos los episodios perdidos", + "SeasonInformation": "Información de temporada", + "SeasonNumber": "Número de temporada", + "SeasonCount": "Recuento de temporada", + "SelectDownloadClientModalTitle": "{modalTitle} - Seleccionar cliente de descarga", + "SelectEpisodes": "Seleccionar episodio(s)", + "SeriesDetailsGoTo": "Ir a {title}", + "SeriesTypes": "Tipos de serie", + "SeriesTypesHelpText": "El tipo de serie es usado para renombrar, analizar y buscar", + "SingleEpisodeInvalidFormat": "Episodio individual: Formato inválido", + "SslCertPasswordHelpText": "Contraseña para el archivo pfx", + "SslPort": "Puerto SSL", + "StandardEpisodeFormat": "Formato de episodio estándar", + "StartProcessing": "Iniciar procesamiento", + "SupportedListsMoreInfo": "Para más información en las listas individuales, haz clic en los botones de más información.", + "TagDetails": "Detalles de etiqueta - {label}", + "Total": "Total", + "True": "Verdadero", + "Umask770Description": "{octal} - Usuario y grupo escriben", + "UsenetBlackholeNzbFolder": "Carpeta Nzb", + "UsenetDelay": "Retraso de usenet", + "UsenetBlackhole": "Blackhole de usenet", + "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} reportó archivos en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.", + "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.", + "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones de tu cliente de descarga.", + "RemoveFilter": "Eliminar filtro", + "RemoveQueueItemConfirmation": "¿Estás seguro que quieres eliminar '{sourceTitle}' de la cola?", + "RemoveRootFolder": "Eliminar la carpeta raíz", + "RemoveSelectedItem": "Eliminar elemento seleccionado", + "RemoveTagsAutomaticallyHelpText": "Eliminar etiquetas automáticamente si las condiciones no se cumplen", + "RemovedFromTaskQueue": "Eliminar de la cola de tareas", + "RemovedSeriesMultipleRemovedHealthCheckMessage": "Las series {series} fueron eliminadas de TheTVDB", + "RenameFiles": "Renombrar archivos", + "ResetAPIKeyMessageText": "¿Estás seguro que quieres restablecer tu clave API?", + "ResetDefinitions": "Restablecer definiciones", + "ResetDefinitionTitlesHelpText": "Restablecer títulos de definición también como valores", + "ResetQualityDefinitionsMessageText": "¿Estás seguro que quieres restablecer las definiciones de calidad?", + "RestartNow": "Reiniciar ahora", + "RestartRequiredToApplyChanges": "{appName} requiere reiniciar para aplicar cambios. ¿Quieres reiniciar ahora?", + "RestartSonarr": "Reiniciar {appName}", + "RestoreBackup": "Restaurar copia de seguridad", + "Result": "Resultado", + "RetryingDownloadOn": "Reintentar descarga en {date} a las {time}", + "Rss": "RSS", + "SaveChanges": "Guardar cambios", + "SceneNumbering": "Numeración de escena", + "Script": "Script", + "Search": "Buscar", + "SearchForMonitoredEpisodesSeason": "Buscar episodios monitorizados en esta temporada", + "SearchForQuery": "Buscar {query}", + "SeasonFolder": "Carpeta de temporada", + "SeasonPassEpisodesDownloaded": "{episodeFileCount}/{totalEpisodeCount} episodios descargados", + "SelectDropdown": "Seleccionar...", + "SelectLanguageModalTitle": "{modalTitle} - Seleccionar idioma", + "SelectLanguages": "Seleccionar idiomas", + "SelectReleaseGroup": "Seleccionar grupo de lanzamiento", + "SeriesDetailsNoEpisodeFiles": "Sin archivos de episodio", + "SeriesFolderImportedTooltip": "Episodio importado de la carpeta de serie", + "SeriesIsMonitored": "La serie está monitorizada", + "SeriesLoadError": "No se pudo cargar la serie", + "SeriesIsUnmonitored": "La serie no está monitorizada", + "SetPermissionsLinuxHelpTextWarning": "Si no estás seguro qué configuraciones hacer, no las cambies.", + "SetPermissionsLinuxHelpText": "¿Debería ejecutarse chmod cuando los archivos son importados/renombrados?", + "SetReleaseGroup": "Establecer grupo de lanzamiento", + "ShowEpisodeInformationHelpText": "Muestra el título y número de episodio", + "ShowMonitoredHelpText": "Muestra el estado monitorizado bajo el póster", + "ShowQualityProfile": "Mostrar perfil de calidad", + "ShowQualityProfileHelpText": "Muestra el perfil de calidad bajo el póster", + "ShowRelativeDates": "Mostrar fechas relativas", + "OnImport": "Al importar", + "Other": "Otro", + "ShowRelativeDatesHelpText": "Muestra fechas absolutas o relativas (Hoy/Ayer/etc)", + "Proxy": "Proxy", + "ShowSearch": "Mostrar búsqueda", + "ShowSearchHelpText": "Muestra el botón de búsqueda al pasar por encima", + "ShowSeasonCount": "Muestra el recuento de temporada", + "ShowAdvanced": "Mostrar avanzado", + "Socks4": "Socks4", + "Socks5": "Socks5 (Soporta TOR)", + "ShowTitle": "Mostrar título", + "Unknown": "Desconocido", + "Sort": "Ordenar", + "SourcePath": "Ruta de la fuente", + "SourceRelativePath": "Ruta relativa de la fuente", + "Special": "Especial", + "SourceTitle": "Título de la fuente", + "SpecialEpisode": "Episodio especial", + "Specials": "Especiales", + "SpecialsFolderFormat": "Formato de carpeta de los especiales", + "SslCertPassword": "Contraseña de certificado SSL", + "SupportedCustomConditions": "{appName} soporta condiciones personalizadas para las siguientes propiedades de lanzamiento.", + "SupportedDownloadClients": "{appName} soporta muchos torrent populares y clientes de descarga de usenet.", + "SupportedIndexers": "{appName} soporta cualquier indexador que use el estándar Newznab, así como otros indexadores listados a continuación.", + "OnSeriesDelete": "Al borrar series", + "OnRename": "Al renombrar", + "OutputPath": "Ruta de salida", + "PreferAndUpgrade": "Preferir y actualizar", + "Presets": "Preajustes", + "ProxyPasswordHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "QueueLoadError": "Fallo al cargar la cola", + "ReadTheWikiForMoreInformation": "Lee la Wiki para más información", + "RecyclingBinCleanupHelpText": "Establece a 0 para deshabilitar la limpieza automática", + "RegularExpressionsTutorialLink": "Más detalles de las expresiones regulares pueden ser encontradas [aquí]({url}).", + "RejectionCount": "Recuento de rechazos", + "RemotePathMappingFileRemovedHealthCheckMessage": "El fichero {path} ha sido eliminado durante el proceso.", + "RemotePathMappings": "Mapeos de ruta remota", + "RemovedSeriesSingleRemovedHealthCheckMessage": "La serie {series} fue eliminada de TheTVDB", + "ReplaceWithDash": "Reemplazar con guion", + "ReplaceWithSpaceDash": "Reemplazar por barra espaciadora", + "ReplaceWithSpaceDashSpace": "Reemplazar por espacio en la barra espaciadora", + "ScriptPath": "Ruta del script", + "SeasonDetails": "Detalles de temporada", + "SecretToken": "Token secreto", + "SelectQuality": "Seleccionar calidad", + "SelectLanguage": "Seleccionar idioma", + "SelectSeason": "Seleccionar temporada", + "SeriesIndexFooterMissingUnmonitored": "Episodios perdidos (Serie no monitorizada)", + "ShowEpisodeInformation": "Mostrar información de episodio", + "ShowPath": "Mostrar ruta", + "ShowNetwork": "Mostrar red", + "TvdbIdExcludeHelpText": "La ID de TVDB de la serie a excluir", + "UpdateSonarrDirectlyLoadError": "No se pudo actualizar {appName} directamente,", + "UpgradesAllowedHelpText": "Si se deshabilita las calidades no serán actualizadas", + "WithFiles": "Con archivos", + "SystemTimeHealthCheckMessage": "La hora del sistema está desfasada más de 1 día. Las tareas programadas pueden no ejecutarse correctamente hasta que la hora sea corregida", + "TableColumns": "Columnas", + "TableColumnsHelpText": "Elige qué columnas son visibles en qué orden aparecen", + "TablePageSize": "Tamaño de página", + "TablePageSizeHelpText": "Número de elementos a mostrar en cada página", + "TablePageSizeMinimum": "El tamaño de página debe ser al menos {minimumValue}", + "TablePageSizeMaximum": "El tamaño de página no debe exceder {maximumValue}", + "TagIsNotUsedAndCanBeDeleted": "La etiqueta no se usa y puede ser borrada", + "TagsSettingsSummary": "Vea todas las etiquetas y cómo se usan. Las etiquetas sin usar pueden ser eliminadas", + "TaskUserAgentTooltip": "User-Agent proporcionado por la aplicación que llamó a la API", + "Test": "Prueba", + "TestAllIndexers": "Probar todos los indexadores", + "TestAllLists": "Probar todas las listas", + "TestParsing": "Probar análisis", + "ThemeHelpText": "Cambiar el tema de la interfaz de la aplicación, el tema 'Auto' usará el tema de tu sistema para establecer el modo luminoso u oscuro. Inspirado por Theme.Park", + "TimeLeft": "Tiempo restante", + "ToggleMonitoredSeriesUnmonitored ": "No se puede conmutar el estado monitorizado cuando la serie no está monitorizada", + "Tomorrow": "Mañana", + "TorrentBlackhole": "Blackhole de torrent", + "TorrentBlackholeSaveMagnetFiles": "Guardar archivos magnet", + "TorrentBlackholeSaveMagnetFilesExtension": "Guardar extensión de archivos magnet", + "TorrentBlackholeSaveMagnetFilesReadOnly": "Solo lectura", + "TorrentBlackholeTorrentFolder": "Carpeta de torrent", + "TorrentDelayHelpText": "Retraso en minutos a esperar antes de capturar un torrent", + "TorrentDelayTime": "Retraso torrent: {torrentDelay}", + "Umask755Description": "{octal} - Usuario escribe, Todos los demás leen", + "Umask777Description": "{octal} - Todos escriben", + "UnableToLoadAutoTagging": "No se pudo cargar el etiquetado automático", + "UnableToLoadBackups": "No se pudo cargar las copias de seguridad", + "Ungroup": "Sin agrupar", + "UnknownDownloadState": "Estado de descarga desconocido: {state}", + "Unlimited": "Ilimitado", + "UnmappedFilesOnly": "Solo archivos sin mapear", + "UnmonitorDeletedEpisodes": "Dejar de monitorizar episodios borrados", + "UnmonitoredOnly": "Solo sin monitorizar", + "UnsavedChanges": "Cambios sin guardar", + "UnselectAll": "Desmarcar todo", + "Upcoming": "Próximamente", + "UpcomingSeriesDescription": "Series que han sido anunciadas pero aún no hay fecha de emisión exacta", + "ReleaseSceneIndicatorUnknownSeries": "Episodio o serie desconocido.", + "RemoveDownloadsAlert": "Las opciones de Eliminar fueron movidas a las opciones del cliente de descarga individual en la table anterior.", + "RestartRequiredHelpTextWarning": "Requiere reiniciar para que tenga efecto", + "SelectFolder": "Seleccionar carpeta", + "TestAllClients": "Probar todos los clientes", + "UpdateFiltered": "Actualizar filtrados", + "SeriesEditor": "Editor de serie", + "Updates": "Actualizaciones", + "NotificationsKodiSettingsDisplayTimeHelpText": "Durante cuánto tiempo serán mostradas las notificaciones (en segundos)", + "NotificationsNtfySettingsUsernameHelpText": "Usuario opcional", + "NotificationsSimplepushSettingsEvent": "Evento", + "NotificationsSimplepushSettingsEventHelpText": "Personaliza el comportamiento de las notificaciones push", + "NotificationsTwitterSettingsConsumerSecret": "Secreto de consumidor", + "NotificationsTelegramSettingsSendSilently": "Enviar de forma silenciosa", + "NotificationsValidationInvalidHttpCredentials": "Credenciales de autenticación HTTP inválidas: {exceptionMessage}", + "OnEpisodeFileDeleteForUpgrade": "Al borrar un archivo de episodio para actualización", + "OnHealthIssue": "Al haber un problema de salud", + "Organize": "Organizar", + "OrganizeRenamingDisabled": "El renombrado está deshabilitado, nada que renombrar", + "OrganizeNothingToRename": "¡Éxito! Mi trabajo está hecho, no hay archivos que renombrar.", + "OrganizeRelativePaths": "Todas las rutas son relativas a: `{path}`", + "Pending": "Pendiente", + "QualityDefinitions": "Definiciones de calidad", + "RecyclingBin": "Papelera de reciclaje", + "ReleaseTitle": "Título de lanzamiento", + "RemotePathMappingLocalPathHelpText": "Ruta que {appName} debería usar para acceder a la ruta remota localmente", + "Remove": "Eliminar", + "RetentionHelpText": "Solo usenet: Establece a cero para establecer una retención ilimitada", + "SelectIndexerFlags": "Seleccionar banderas del indexador", + "SelectSeasonModalTitle": "{modalTitle} - Seleccionar temporada", + "SeriesFinale": "Final de serie", + "SeriesAndEpisodeInformationIsProvidedByTheTVDB": "La información de serie y episodio es proporcionada por TheTVDB.com. [Por favor considera apoyarlos]({url}).", + "SetIndexerFlags": "Establecer banderas del indexador", + "SkipRedownload": "Saltar redescarga", + "ShowMonitored": "Mostrar monitorizado", + "Space": "Espacio", + "TimeFormat": "Formato de hora", + "UiSettings": "Opciones de interfaz", + "Umask": "UMask", + "UpdateStartupNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' no es modificable por el usuario '{userName}'.", + "UsenetDelayHelpText": "Retraso en minutos a esperar antes de capturar un lanzamiento desde usenet", + "PartialSeason": "Temporada parcial", + "RemoveSelectedItemQueueMessageText": "¿Estás seguro que quieres eliminar 1 elemento de la cola?", + "SceneInformation": "Información de escena", + "UpgradeUntilThisQualityIsMetOrExceeded": "Actualizar hasta que esta calidad sea alcanzada o excedida", + "Uppercase": "Mayúsculas", + "SeriesDetailsRuntime": "{runtime} minutos", + "ShowBannersHelpText": "Muestra banners en lugar de títulos", + "SslCertPathHelpText": "Ruta al archivo pfx", + "Umask750Description": "{octal} - Usuario escribe, Grupo lee", + "UrlBaseHelpText": "Para soporte de proxy inverso, por defecto está vacío", + "UpdateAll": "Actualizar todo", + "ConnectionSettingsUrlBaseHelpText": "Añade un prefijo a la url {connectionName}, como {url}", + "UsenetDelayTime": "Retraso de usenet: {usenetDelay}", + "UsenetDisabled": "Usenet deshabilitado", + "Username": "Usuario", + "UtcAirDate": "Fecha de emisión UTC", + "Version": "Versión", + "WaitingToImport": "Esperar para importar", + "NotificationsDiscordSettingsOnGrabFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al capturar'", + "NotificationsNtfyValidationAuthorizationRequired": "Se requiere autorización", + "NotificationsNtfySettingsClickUrl": "URL al hacer clic", + "NotificationsNotifiarrSettingsApiKeyHelpText": "Tu clave API de tu perfil", + "NotificationsPlexSettingsAuthenticateWithPlexTv": "Autenticar con Plex.tv", + "NotificationsPushcutSettingsNotificationNameHelpText": "Nombre de notificación de la pestaña Notificaciones de la aplicación Pushcut", + "NotificationsPlexValidationNoTvLibraryFound": "Al menos se requiere una biblioteca de TV", + "NotificationsPushBulletSettingSenderId": "ID del remitente", + "NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "ID de grupo / Número de teléfono del receptor", + "NotificationsSettingsWebhookMethod": "Método", + "NotificationsSettingsUseSslHelpText": "Conectar a {serviceName} sobre HTTPS en vez de HTTP", + "NotificationsTwitterSettingsConsumerSecretHelpText": "Secreto de consumidor de una aplicación de Twitter", + "NotificationsValidationInvalidUsernamePassword": "Usuario o contraseña inválido", + "NotificationsSlackSettingsWebhookUrlHelpText": "URL de canal webhook de Slack", + "PackageVersion": "Versión del paquete", + "NotificationsValidationUnableToConnectToApi": "No se pudo conectar a la API de {service}. La conexión al servidor falló: ({responseCode}) {exceptionMessage}", + "PosterOptions": "Opciones de póster", + "PreferTorrent": "Preferir torrent", + "PreviewRenameSeason": "Previsualizar renombrado para esta temporada", + "PreviousAiring": "Emisiones anteriores", + "RemoveFromDownloadClient": "Eliminar del cliente de descarga", + "RemovingTag": "Eliminando etiqueta", + "Required": "Solicitado", + "Reorder": "Reordenar", + "SceneInfo": "Información de escena", + "RootFolderMissingHealthCheckMessage": "Carpeta raíz perdida: {rootFolderPath}", + "SearchAll": "Buscar todo", + "SelectAll": "Seleccionar todo", + "SeriesIndexFooterEnded": "FInalizado (Todos los episodios descargados)", + "ShowDateAdded": "Mostrar fecha de adición", + "UnmonitorDeletedEpisodesHelpText": "Los episodios borrados del disco son dejados de monitorizar automáticamente en {appName}", + "UnmonitorSelected": "Dejar de monitorizar seleccionados", + "UpdateSelected": "Actualizar seleccionados", + "UpdateUiNotWritableHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de interfaz '{uiFolder}' no es modificable por el usuario '{userName}'.", + "UpgradeUntil": "Actualizar hasta", + "UpdaterLogFiles": "Actualizador de archivos de registro", + "UseSeasonFolder": "Usar carpeta de temporada", + "UseHardlinksInsteadOfCopy": "Utilizar enlaces directos en lugar de copiar", + "View": "Vista", + "VisitTheWikiForMoreDetails": "Visita la wiki para más detalles: ", + "WaitingToProcess": "Esperar al proceso", + "Week": "Semana", + "WeekColumnHeader": "Cabecera de columna de semana", + "Release": "Lanzamiento", + "RemoveSelectedItems": "Eliminar elementos seleccionados", + "RemoveSelectedItemsQueueMessageText": "¿Estás seguro que quieres eliminar {selectedCount} elementos de la cola?", + "RootFolderSelectFreeSpace": "{freeSpace} libres", + "RootFolderPath": "Ruta de carpeta raíz", + "RssSyncInterval": "Intervalo de sincronización RSS", + "SingleEpisode": "Episodio individual", + "ShowUnknownSeriesItems": "Mostrar elementos de serie desconocidos", + "NotificationsGotifySettingIncludeSeriesPoster": "Incluir poster de serie", + "NotificationsKodiSettingsCleanLibraryHelpText": "Limpia la biblioteca después de actualizar", + "NotificationsKodiSettingsCleanLibrary": "Limpiar biblioteca", + "NotificationsKodiSettingsGuiNotification": "Notificación de interfaz gráfica", + "NotificationsKodiSettingsUpdateLibraryHelpText": "¿Actualiza la biblioteca durante Importar y renombrar?", + "NotificationsMailgunSettingsUseEuEndpointHelpText": "Habilitar el uso del endpoint de UE de MailGun", + "NotificationsMailgunSettingsUseEuEndpoint": "Usar el endpoint de la UE", + "NotificationsNtfySettingsAccessToken": "Token de acceso", + "NotificationsNtfySettingsAccessTokenHelpText": "Autorización opcional basada en token. Tiene prioridad sobre usuario/contraseña", + "NotificationsNtfySettingsTagsEmojisHelpText": "Lista opcional de etiquetas o emojis para usar", + "NotificationsNtfySettingsTopics": "Temas", + "NotificationsPushBulletSettingsDeviceIds": "IDs de dispositivo", + "NotificationsPushBulletSettingsAccessToken": "Token de acceso", + "NotificationsPushBulletSettingsChannelTags": "Etiquetas de canal", + "NotificationsPushcutSettingsTimeSensitive": "Sensible al tiempo", + "NotificationsPushcutSettingsTimeSensitiveHelpText": "Habilitar para marcas la notificación como \"Sensible al tiempo\"", + "NotificationsPushoverSettingsDevices": "Dispositivos", + "NotificationsPushoverSettingsDevicesHelpText": "Lista de nombres de dispositivo (deja en blanco para enviar a todos los dispositivos)", + "NotificationsPushoverSettingsExpireHelpText": "Tiempo máximo para reintentar las alertas de emergencia, máximo 86400 segundos", + "NotificationsPushoverSettingsRetry": "Reintentar", + "NotificationsPushoverSettingsSound": "Sonido", + "NotificationsPushoverSettingsUserKey": "Clave de usuario", + "NotificationsPushoverSettingsSoundHelpText": "Sonido de notificación, deja en blanco para usar el predeterminado", + "NotificationsSettingsWebhookMethodHelpText": "Qué método HTTP utilizar para enviar al servicio web", + "NotificationsSettingsWebhookUrl": "URL del webhook", + "NotificationsSignalSettingsGroupIdPhoneNumber": "ID de grupo / Número de teléfono", + "NotificationsSignalSettingsPasswordHelpText": "Contraseña usada para autenticar solicitudes hacia signal-api", + "NotificationsSignalSettingsSenderNumber": "Número del emisor", + "NotificationsSignalSettingsUsernameHelpText": "Usuario usado para autenticar solicitudes hacia signal-api", + "NotificationsSignalValidationSslRequired": "Se requiere SSL", + "NotificationsSimplepushSettingsKey": "Clave", + "NotificationsSlackSettingsChannel": "Canal", + "NotificationsSlackSettingsIconHelpText": "Cambia el icono usado para mensajes publicados a Slack (emoji o URL)", + "NotificationsSlackSettingsUsernameHelpText": "Usuario para publicar a Slack", + "NotificationsTelegramSettingsSendSilentlyHelpText": "Envía el mensaje de forma silenciosa. Los usuarios recibirán una notificación sin sonido", + "NotificationsTelegramSettingsTopicId": "ID de tema", + "NotificationsTraktSettingsAuthenticateWithTrakt": "Autenticar con Trakt", + "NotificationsTraktSettingsExpires": "Caduca", + "NotificationsValidationUnableToConnectToService": "No se pudo conectar a {serviceName}", + "NotificationsValidationUnableToSendTestMessage": "No se pudo enviar un mensaje de prueba: {exceptionMessage}", + "NzbgetHistoryItemMessage": "Estado de PAR: {parStatus} - Estado de desempaquetado: {unpackStatus} - Estado de movido: {moveStatus} - Estado de script: {scriptStatus} - Estado de borrado: {deleteStatus} - Estado de marcado: {markStatus}", + "OpenSeries": "Abrir serie", + "OrganizeLoadError": "Error cargando vistas previas", + "OrganizeNamingPattern": "Patrón de nombrado: `{episodeFormat}`", + "OverrideGrabNoSeries": "La serie debe ser seleccionada", + "PackageVersionInfo": "{packageVersion} por {packageAuthor}", + "PendingChangesDiscardChanges": "Descartar cambios y salir", + "Period": "Periodo", + "PendingChangesMessage": "Tienes cambios sin guardar. ¿Estás seguro que quieres salir de esta página?", + "PreviouslyInstalled": "Previamente instalado", + "ProtocolHelpText": "Elige qué protocolo(s) usar y cuál se prefiere cuando se elige entre lanzamientos equivalentes", + "ProgressBarProgress": "Barra de progreso al {progress}%", + "ProxyBypassFilterHelpText": "Usa ',' como separador, y '*.' como comodín para subdominios", + "ProxyResolveIpHealthCheckMessage": "Fallo al resolver la dirección IP para el host proxy configurado {proxyHostName}", + "ProxyUsernameHelpText": "Solo necesitas introducir un usuario y contraseña si se requiere alguno. De otra forma déjalos en blanco.", + "QualityProfile": "Perfil de calidad", + "QualityDefinitionsLoadError": "No se pudo cargar las definiciones de calidad", + "QualityProfiles": "Perfiles de calidad", + "QualityProfilesLoadError": "No se pudo cargar los perfiles de calidad", + "QueueFilterHasNoItems": "Seleccionado filtro de cola que no tiene elementos", + "QuickSearch": "Búsqueda rápida", + "Real": "Real", + "Reason": "Razón", + "RegularExpression": "Expresión regular", + "ReleaseHash": "Hash de lanzamiento", + "Rejections": "Rechazos", + "RecyclingBinCleanupHelpTextWarning": "Los archivos en la papelera de reciclaje anteriores al número de días seleccionado serán limpiados automáticamente", + "ReleaseProfiles": "Perfiles de lanzamiento", + "ReleaseRejected": "Lanzamiento rechazado", + "ReleaseSceneIndicatorAssumingScene": "Asumiendo numeración de escena.", + "ReleaseSceneIndicatorAssumingTvdb": "Asumiendo numeración de TVDB.", + "ReleaseSceneIndicatorUnknownMessage": "La numeración varía para este episodio y el lanzamiento no coincide con ningún mapeo conocido.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} puede ver pero no acceder al episodio descargado {path}. Probablemente error de permisos.", + "RemotePathMappingRemotePathHelpText": "Ruta raíz al directorio al que accede el cliente de descarga", + "RemoveFailedDownloads": "Eliminar descargas fallidas", + "RemoveSelected": "Eliminar seleccionado", + "RenameEpisodesHelpText": "{appName} usará el nombre de archivo existente si el renombrado está deshabilitado", + "RenameEpisodes": "Renombrar episodios", + "RestrictionsLoadError": "No se pudo cargar Restricciones", + "SearchForMissing": "Buscar perdidos", + "SeasonFinale": "Final de temporada", + "SearchSelected": "Buscar seleccionados", + "SeasonFolderFormat": "Formato de carpeta de temporada", + "SendAnonymousUsageData": "Enviar datos de uso anónimos", + "SeriesDetailsOneEpisodeFile": "1 archivo de episodio", + "SeriesFolderFormatHelpText": "Usado cuando se añade una nueva serie o se mueve la serie a través del editor de serie", + "SeriesID": "ID de serie", + "SetPermissions": "Establecer permisos", + "SetReleaseGroupModalTitle": "{modalTitle} - Establecer grupo de lanzamiento", + "SetTags": "Establecer etiquetas", + "ShowPreviousAiring": "Mostrar emisión anterior", + "ShowSizeOnDisk": "Mostrar tamaño en disco", + "SizeOnDisk": "Tamaño en disco", + "SizeLimit": "Límite de tamaño", + "SkipRedownloadHelpText": "Evita que {appName} intente descargar un lanzamiento alternativo para este elemento", + "Small": "Pequeño", + "SomeResultsAreHiddenByTheAppliedFilter": "Algunos resultados están ocultos por el filtro aplicado", + "SonarrTags": "Etiquetas de {appName}", + "Standard": "Estándar", + "StandardEpisodeTypeFormat": "Temporada y número de episodios ({format})", + "StandardEpisodeTypeDescription": "Episodios lanzados con patrón SxxEyy", + "SubtitleLanguages": "Idiomas de subtítulo", + "SupportedAutoTaggingProperties": "{appName} soporta las siguientes propiedades para reglas de etiquetado automáticas", + "SupportedIndexersMoreInfo": "Para más información en los indexadores individuales, haz clic en los botones de más información.", + "SupportedListsSeries": "{appName} soporta múltiples listas para importar series en la base de datos.", + "TableOptions": "Opciones de tabla", + "TableOptionsButton": "Botón de opciones de tabla", + "Today": "Hoy", + "Titles": "Títulos", + "ToggleUnmonitoredToMonitored": "Sin monitorizar, haz clic para monitorizar", + "TotalFileSize": "Tamaño total de archivo", + "UpdateAvailableHealthCheckMessage": "Hay disponible una nueva actualización", + "UpgradeUntilCustomFormatScore": "Actualizar hasta la puntuación de formato personalizado", + "UrlBase": "URL base", + "UseSsl": "Usar SSL", + "Usenet": "Usenet", + "VersionNumber": "Versión {version}", + "OnManualInteractionRequired": "Cuando se requiera interacción manual", + "OnLatestVersion": "La última versión de {appName} ya está instalada", + "OnUpgrade": "Al actualizar", + "RootFolders": "Carpetas raíz", + "SeasonPremiere": "Estreno de temporada", + "UnableToUpdateSonarrDirectly": "No se pudo actualizar {appName} directamente,", + "UnmappedFolders": "Carpetas sin mapear", + "QualitiesLoadError": "No se pudo cargar las calidades", + "SeasonNumberToken": "Temporada {seasonNumber}", + "PreferredSize": "Tamaño preferido", + "TypeOfList": "Lista {typeOfList}", + "UiSettingsLoadError": "No se pudo cargar las opciones de interfaz", + "UpdateMonitoring": "Actualizar monitorizando", + "ReleaseType": "Tipo de lanzamiento", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "El cliente de descarga local {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa las opciones de tu cliente de descarga.", + "RemotePathMappingWrongOSPathHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remota y las opciones del cliente de descarga.", + "RemoveFailedDownloadsHelpText": "Eliminar descargas fallidas desde el historial del cliente de descarga", + "RemoveFromQueue": "Eliminar de la cola", + "RemoveMultipleFromDownloadClientHint": "Elimina descargas y archivos del cliente de descarga", + "RemoveQueueItemsRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará las descargas y los archivos del cliente de descarga.", + "RemoveTagsAutomatically": "Eliminar etiquetas automáticamente", + "ReplaceIllegalCharactersHelpText": "Reemplaza los caracteres ilegales. Si no está marcado, {appName} los eliminará en su lugar", + "ResetAPIKey": "Restablecer clave API", + "RootFolder": "Carpeta raíz", + "RootFolderMultipleMissingHealthCheckMessage": "Múltiples carpetas raíz están perdidas: {rootFolderPaths}", + "RestartReloadNote": "Nota: {appName} se reiniciará automáticamente y recargará la interfaz durante el proceso de restauración.", + "RestartRequiredWindowsService": "Dependiendo de qué usuario esté ejecutando el servicio {appName}, puede ser necesario reiniciar {appName} como administrador antes de que el servicio se inicie automáticamente.", + "SeasonPremieresOnly": "Solo estrenos de temporada", + "SeasonPassTruncated": "Solo se muestran las últimas 25 temporadas, ve a detalles para ver todas las temporadas", + "SelectFolderModalTitle": "{modalTitle} - Seleccionar carpeta", + "SeriesDetailsCountEpisodeFiles": "{episodeFileCount} archivos de episodio", + "SeriesIndexFooterContinuing": "Continuando (Todos los episodios descargados)", + "SetIndexerFlagsModalTitle": "{modalTitle} - Establecer banderas del indexador", + "ShortDateFormat": "Formato de fecha breve", + "ShowUnknownSeriesItemsHelpText": "Muestra elementos sin una serie en la cola, esto incluiría series eliminadas, películas o cualquier cosa más en la categoría de {appName}", + "ShownClickToHide": "Mostrado, haz clic para ocultar", + "SkipFreeSpaceCheckWhenImportingHelpText": "Se usa cuando {appName} no puede detectar el espacio libre de tu carpeta raíz durante la importación de archivo", + "SmartReplace": "Reemplazo inteligente", + "SupportedDownloadClientsMoreInfo": "Para más información en los clientes de descarga individuales, haz clic en los botones de más información.", + "SupportedImportListsMoreInfo": "Para más información de los listas de importación individuales, haz clic en los botones de más información.", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "En lugar de mover archivos esto indicará a {appName} que copie o enlace (dependiendo de los ajustes/configuración del sistema)", + "TorrentDelay": "Retraso de torrent", + "ToggleMonitoredToUnmonitored": "Monitorizado, haz clic para dejar de monitorizar", + "TorrentBlackholeSaveMagnetFilesHelpText": "Guarda el enlace magnet si no hay ningún archivo .torrent disponible (útil solo si el cliente de descarga soporta magnets guardados en un archivo)", + "UiLanguage": "Idioma de interfaz", + "UiLanguageHelpText": "Idioma que {appName} usará en la interfaz", + "UiSettingsSummary": "Opciones de calendario, fecha y color alterado", + "UpdateAutomaticallyHelpText": "Descargar e instalar actualizaciones automáticamente. Todavía puedes instalar desde Sistema: Actualizaciones", + "TotalRecords": "Total de registros: {totalRecords}", + "WantMoreControlAddACustomFormat": "¿Quieres más control sobre qué descargas son preferidas? Añade un [formato personalizado](/opciones/formatospersonalizados)", + "OrganizeModalHeader": "Organizar y renombrar", + "RemoveCompleted": "Eliminar completado", + "OpenBrowserOnStartHelpText": " Abre un navegador web y navega a la página de inicio de {appName} al iniciar la aplicación.", + "SslCertPath": "Ruta del certificado SSL", + "StartImport": "Iniciar importación", + "OptionalName": "Nombre opcional", + "RemotePath": "Ruta remota", + "SeriesPremiere": "Estreno de serie", + "SeriesMatchType": "Tipo de emparejamiento de series", + "SeriesMonitoring": "Monitorización de serie", + "Tba": "TBA", + "TorrentsDisabled": "Torrents deshabilitados", + "RemotePathMappingGenericPermissionsHealthCheckMessage": "El cliente de descarga {downloadClientName} ubica las descargas en {path} pero {appName} no puede ver este directorio. Puede que necesites ajustar los permisos de la carpeta.", + "ReplaceIllegalCharacters": "Reemplazar caracteres ilegales", + "ResetTitles": "Restablecer títulos", + "SmartReplaceHint": "Raya o barra espaciadora según el nombre", + "SelectEpisodesModalTitle": "{modalTitle} - Seleccionar episodio(s)", + "DownloadClientDelugeSettingsDirectory": "Directorio de descarga", + "DownloadClientDelugeSettingsDirectoryHelpText": "Ubicación opcional en la que poner las descargas, dejar en blanco para usar la ubicación predeterminada de Deluge", + "UnmonitorSpecialsEpisodesDescription": "Dejar de monitorizar todos los episodios especiales sin cambiar el estado monitorizado de otros episodios", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicación opcional a la que mover las descargas completadas, dejar en blanco para usar la ubicación predeterminada de Deluge", + "DownloadClientDelugeSettingsDirectoryCompleted": "Directorio al que mover cuando se complete", + "NotificationsDiscordSettingsWebhookUrlHelpText": "URL de canal webhook de Discord", + "NotificationsEmailSettingsCcAddress": "Dirección(es) CC", + "NotificationsEmbySettingsSendNotifications": "Enviar notificaciones", + "NotificationsEmbySettingsUpdateLibraryHelpText": "¿Actualiza biblioteca en importar, renombrar o borrar?", + "NotificationsJoinSettingsDeviceIdsHelpText": "En desuso, usar Nombres de dispositivo en su lugar. Lista separada por coma de los IDs de dispositivo a los que te gustaría enviar notificaciones. Si no se establece, todos los dispositivos recibirán notificaciones.", + "NotificationsPushoverSettingsExpire": "Caduca", + "NotificationsMailgunSettingsSenderDomain": "Dominio del remitente", + "NotificationsNtfySettingsServerUrl": "URL del servidor", + "PreferProtocol": "Preferir {preferredProtocol}", + "ProfilesSettingsSummary": "Perfiles de calidad, de retraso de idioma y de lanzamiento", + "QualitiesHelpText": "Calidades superiores en la lista son más preferibles. Calidades dentro del mismo grupo son iguales. Comprobar solo calidades que se busquen", + "RssIsNotSupportedWithThisIndexer": "RSS no está soportado con este indexador", + "Repack": "Reempaquetar", + "NotificationsGotifySettingsPriorityHelpText": "Prioridad de la notificación", + "NotificationsGotifySettingsServer": "Servidor Gotify", + "NotificationsPlexSettingsAuthToken": "Token de autenticación", + "NotificationsSynologySettingsUpdateLibraryHelpText": "Llamada synoindex en localhost para actualizar un archivo de biblioteca", + "Overview": "Vista general", + "UseSeasonFolderHelpText": "Ordenar episodios en carpetas de temporada", + "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir dentro del contenedor. Revisa tus mapeos de ruta remotos y opciones de volumen del contenedor.", + "Retention": "Retención", + "NotificationsDiscordSettingsOnManualInteractionFields": "Campos durante la interacción manual", + "NotificationsDiscordSettingsOnGrabFields": "Campos al capturar", + "NotificationsDiscordSettingsOnImportFields": "Campos al importar", + "NotificationsDiscordSettingsOnImportFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'al importar'", + "NotificationsDiscordSettingsOnManualInteractionFieldsHelpText": "Cambia los campos que se pasan para esta notificación 'durante la interacción manual'", + "NotificationsEmailSettingsCcAddressHelpText": "Lista separada por coma de destinatarios de e-mail cc", + "NotificationsEmailSettingsFromAddress": "De dirección", + "NotificationsKodiSettingAlwaysUpdateHelpText": "¿Actualiza la biblioteca incluso cuando un video se esté reproduciendo?", + "NotificationsKodiSettingsDisplayTime": "Tiempo de visualización", + "NotificationsLoadError": "No se pudo cargar las notificaciones", + "NotificationsMailgunSettingsApiKeyHelpText": "La clave API generada desde MailGun", + "NotificationsSendGridSettingsApiKeyHelpText": "La clave API generada por SendGrid", + "NotificationsTwitterSettingsConsumerKeyHelpText": "Clave de consumidor de una aplicación de Twitter", + "NotificationsTwitterSettingsDirectMessage": "Mensaje directo", + "NotificationsTwitterSettingsDirectMessageHelpText": "Envía un mensaje directo en lugar de un mensaje público", + "OnApplicationUpdate": "Al actualizar la aplicación", + "OnSeriesAdd": "Al añadir series", + "OnlyForBulkSeasonReleases": "Solo para lanzamientos de temporada a granel", + "OrganizeModalHeaderSeason": "Organizar y renombrar - {season}", + "OverrideGrabNoEpisode": "Al menos un episodio debe ser seleccionado", + "OverrideGrabNoQuality": "La calidad debe ser seleccionada", + "NotificationsValidationInvalidAuthenticationToken": "Token de autenticación inválido", + "NotificationsValidationUnableToConnect": "No se pudo conectar: {exceptionMessage}", + "NotificationsValidationUnableToSendTestMessageApiResponse": "No se pudo enviar un mensaje de prueba. Respuesta de la API: {error}", + "OverrideGrabModalTitle": "Sobrescribe y captura - {title}", + "ReleaseProfileTagSeriesHelpText": "Los perfiles de lanzamientos se aplicarán a series con al menos una etiqueta coincidente. Deja en blanco para aplicar a todas las series", + "ReleaseSceneIndicatorMappedNotRequested": "El episodio mapeado no fue solicitado en esta búsqueda.", + "RemotePathMappingBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} ubica las descargas en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} puede ver pero no acceder al directorio de descarga {downloadPath}. Probablemente error de permisos.", + "RemotePathMappingHostHelpText": "El mismo host que especificaste para el cliente de descarga remoto", + "ParseModalUnableToParse": "No se pudo analizar el título proporcionado, por favor inténtalo de nuevo.", + "Preferred": "Preferido", + "Priority": "Prioridad", + "QualityProfileInUseSeriesListCollection": "No se puede borrar un perfil de calidad que está asignado a una serie, lista o colección", + "ReleaseProfile": "Perfil de lanzamiento", + "ReleaseProfileIndexerHelpText": "Especifica a qué indexador se aplica el perfil", + "RequiredHelpText": "Esta condición {implementationName} debe coincidir para el formato personalizado para aplicar. De otro modo una coincidencia sencilla {implementationName} es suficiente.", + "RemotePathMappingsLoadError": "No se pudo cargar los mapeos de ruta remota", + "RestartLater": "Reiniciaré más tarde", + "RootFoldersLoadError": "No se pudo cargar las carpetas raíz", + "RssSync": "Sincronización RSS", + "RssSyncIntervalHelpTextWarning": "Esto se aplicará a todos los indexadores, por favor sigue las reglas establecidas por ellos", + "Score": "Puntuación", + "SearchFailedError": "La búsqueda falló, por favor inténtalo de nuevo más tarde.", + "SearchForMonitoredEpisodes": "Buscar episodios monitorizados", + "SearchIsNotSupportedWithThisIndexer": "La búsqueda no está soportada con este indexador", + "SearchMonitored": "Buscar monitorizados", + "SeasonPack": "Pack de temporada", + "SeriesCannotBeFound": "Lo siento, esta serie no puede ser encontrada.", + "SeriesEditRootFolderHelpText": "Mover series a la misma carpeta raíz se puede usar para renombrar carpetas de series para coincidir el título actualizado o el formato de nombrado", + "SeriesFolderFormat": "Formato de carpeta de serie", + "SeriesIndexFooterDownloading": "Descargando (Uno o más episodios)", + "SeriesIndexFooterMissingMonitored": "Episodios perdidos (Serie monitorizada)", + "SeriesProgressBarText": "{episodeFileCount} / {episodeCount} (Total: {totalEpisodeCount}, Descargando: {downloadingCount})", + "UpgradesAllowed": "Actualizaciones permitidas", + "VideoCodec": "Códec de vídeo", + "SeriesTitleToExcludeHelpText": "El nombre de la serie a excluir", + "Shutdown": "Apagar", + "TestAll": "Probar todo", + "UseProxy": "Usar proxy", + "Repeat": "Repetir", + "Replace": "Reemplazar", + "RemoveCompletedDownloadsHelpText": "Elimina las descargas importadas desde el historial del cliente de descarga", + "RemoveQueueItemRemovalMethod": "Método de eliminación", + "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "Estás usando docker; el cliente de descarga {downloadClientName} reportó archivos en {path} pero esta no es una ruta {osName} válida. Revisa tus mapeos de ruta remotos y opciones del cliente de descarga.", + "RemoveCompletedDownloads": "Eliminar descargas completadas", + "RemoveFromDownloadClientHint": "Elimina la descarga y archivo(s) del cliente de descarga", + "EpisodeRequested": "Episodio requerido", + "NotificationsEmailSettingsServer": "Servidor", + "NotificationsEmailSettingsServerHelpText": "Nombre de host o IP del servidor de e-mail", + "NotificationsGotifySettingsAppTokenHelpText": "El token de aplicación generado por Gotify", + "NotificationsGotifySettingsServerHelpText": "URL de servidor de Gotify, incluyendo http(s):// y puerto si es necesario", + "NotificationsJoinSettingsDeviceIds": "IDs de dispositivo", + "NotificationsJoinValidationInvalidDeviceId": "Los IDs de dispositivo parecen inválidos.", + "NotificationsKodiSettingAlwaysUpdate": "Actualizar siempre", + "NotificationsPushcutSettingsApiKeyHelpText": "Las claves API pueden ser gestionadas en la vista Cuenta de la aplicación Pushcut", + "NotificationsPushcutSettingsNotificationName": "Nombre de notificación", + "NotificationsPushoverSettingsRetryHelpText": "Intervalo para reintentar las alertas de emergencia, mínimo 30 segundos", + "NotificationsSettingsUpdateLibrary": "Actualizar biblioteca", + "NotificationsSettingsUpdateMapPathsTo": "Mapear rutas a", + "NotificationsSignalSettingsSenderNumberHelpText": "Número de teléfono del emisor registrado en signal-api", + "NotificationsSlackSettingsChannelHelpText": "Sobrescribe el canal predeterminado para el webhook entrante (#otro-canal)", + "NotificationsSlackSettingsIcon": "Icono", + "NotificationsSynologyValidationInvalidOs": "Debe ser un Synology", + "NotificationsSynologyValidationTestFailed": "No es Synology o synoindex no está disponible", + "NotificationsTelegramSettingsBotToken": "Token de bot", + "NotificationsTelegramSettingsChatId": "ID de chat", + "NotificationsTelegramSettingsTopicIdHelpText": "Especifica una ID de tema para enviar notificaciones a ese tema. Deja en blanco para usar el tema general (solo supergrupos)", + "NotificationsTraktSettingsAccessToken": "Token de acceso", + "NotificationsTraktSettingsAuthUser": "Autenticar usuario", + "NotificationsTwitterSettingsAccessToken": "Token de acceso", + "NotificationsTwitterSettingsAccessTokenSecret": "Token secreto de acceso", + "NotificationsTwitterSettingsConsumerKey": "Clave de consumidor", + "NotificationsTwitterSettingsMention": "Mención", + "NotificationsTwitterSettingsMentionHelpText": "Menciona este usuario en tweets enviados", + "NotificationsValidationInvalidAccessToken": "Token de acceso inválido", + "NotificationsValidationInvalidApiKey": "Clave API inválida", + "ParseModalErrorParsing": "Error analizando, por favor inténtalo de nuevo.", + "ParseModalHelpText": "Introduce un título de lanzamiento en la entrada anterior", + "SearchByTvdbId": "También puedes buscar usando la ID de TVDB de un show. P. ej. tvdb:71663", + "SearchForAllMissingEpisodesConfirmationCount": "¿Estás seguro que quieres buscar los {totalRecords} episodios perdidos?", + "SeriesType": "Tipo de serie", + "TagCannotBeDeletedWhileInUse": "La etiqueta no puede ser borrada mientras esté en uso", + "UnmonitorSpecialEpisodes": "Dejar de monitorizar especiales", + "UpdateStartupTranslocationHealthCheckMessage": "No se puede instalar la actualización porque la carpeta de inicio '{startupFolder}' está en una carpeta de translocalización de la aplicación.", + "Yesterday": "Ayer", + "RemoveQueueItemRemovalMethodHelpTextWarning": "'Eliminar del cliente de descarga' eliminará la descarga y el archivo(s) del cliente de descarga.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "El cliente de descarga remoto {downloadClientName} ubica las descargas en {path} pero este directorio no parece existir. Probablemente mapeo de ruta remota perdido o incorrecto.", + "RemotePathMappingsInfo": "Los mapeos de ruta remota son muy raramente solicitados, si {appName} y tu cliente de descarga están en el mismo sistema es mejor coincidir sus rutas. Para más información mira la [wiki]({wikiLink})", + "UpdateScriptPathHelpText": "Ruta a un script personalizado que toma un paquete de actualización extraído y gestiona el resto del proceso de actualización", + "NotificationsTelegramSettingsChatIdHelpText": "Debes comenzar una conversación con el bot o añádelo a tu grupo para recibir mensajes", + "NotificationsEmailSettingsRecipientAddressHelpText": "Lista separada por coma de destinatarios de e-mail", + "NotificationsTwitterSettingsConnectToTwitter": "Conectar a Twitter / X", + "NotificationsValidationInvalidApiKeyExceptionMessage": "Clave API inválida: {exceptionMessage}", + "NotificationsJoinSettingsApiKeyHelpText": "La clave API de tus ajustes de Añadir cuenta (haz clic en el botón Añadir API).", + "NotificationsPushBulletSettingsDeviceIdsHelpText": "Lista de IDs de dispositivo (deja en blanco para enviar a todos los dispositivos)", + "NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')", + "ReleaseProfileIndexerHelpTextWarning": "Establecer un indexador específico en un perfil de lanzamiento provocará que este perfil solo se aplique a lanzamientos desde ese indexador.", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar con MyAnimeList", + "ImportListsMyAnimeListSettingsListStatus": "Estado de lista", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista desde la que quieres importar, establecer a 'Todo' para todas las listas", + "CustomFormatsSettingsTriggerInfo": "Un formato personalizado será aplicado al lanzamiento o archivo cuando coincida con al menos uno de los diferentes tipos de condición elegidos.", + "ClickToChangeReleaseType": "Haz clic para cambiar el tipo de lanzamiento", + "ReleaseGroupFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Grupo de lanzamiento:30}`) como desde el principio (p. ej. `{Grupo de lanzamiento:-30}`).", + "SelectReleaseType": "Seleccionar tipo de lanzamiento", + "SeriesFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de serie:30}`) como desde el principio (p. ej. `{Título de serie:-30}`).", + "EpisodeTitleFootNote": "Opcionalmente controla el truncamiento hasta un número máximo de bytes, incluyendo elipsis (`...`). Está soportado truncar tanto desde el final (p. ej. `{Título de episodio:30}`) como desde el principio (p. ej. `{Título de episodio:-30}`). Los títulos de episodio serán truncados automáticamente acorde a las limitaciones del sistema de archivos si es necesario.", + "AutoTaggingSpecificationTag": "Etiqueta", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} en el título", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Prefija opcionalmente el título de mensaje con {appName} para diferenciar notificaciones de aplicaciones diferentes" } diff --git a/src/NzbDrone.Core/Localization/Core/fi.json b/src/NzbDrone.Core/Localization/Core/fi.json index af390db5a..adbb36ef8 100644 --- a/src/NzbDrone.Core/Localization/Core/fi.json +++ b/src/NzbDrone.Core/Localization/Core/fi.json @@ -2,7 +2,7 @@ "RecycleBinUnableToWriteHealthCheckMessage": "Määritettyyn roskakorikansioon ei voida tallentaa: {path}. Varmista että sijainti on olemassa ja että sovelluksen suorittavalla käyttäjällä on siihen kirjoitusoikeus.", "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} näkee ladatun jakson \"{path}\", mutta ei voi avata sitä. Tämä johtuu todennäköisesti liian rajallisista käyttöoikeuksista.", "Added": "Lisäysaika", - "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää.", + "AppDataLocationHealthCheckMessage": "Päivityksiä ei sallita, jotta AppData-kansion poistaminen päivityksen yhteydessä voidaan estää", "DownloadClientSortingHealthCheckMessage": "Lataustyökalun \"{downloadClientName}\" {sortingMode} on kytketty käyttöön {appName}in kategorialle ja tuontiongelmien välttämiseksi se tulisi poistaa käytöstä.", "IndexerRssNoIndexersEnabledHealthCheckMessage": "RSS-synkronointia varten ei ole määritetty tietolähteitä ja tämän vuoksi {appName} ei kaappaa uusia julkaisuja automaattisesti.", "IndexerSearchNoInteractiveHealthCheckMessage": "Manuaalihaulle ei ole määritetty tietolähteitä, eikä se sen vuoksi löydä tuloksia.", @@ -15,7 +15,7 @@ "GrabId": "Kaappauksen tunniste", "BindAddressHelpText": "Toimiva IP-osoite, localhost tai * (tähti) kaikille verkkoliitännöille.", "BrowserReloadRequired": "Käyttöönotto vaatii selaimen sivupäivityksen.", - "CustomFormatHelpText": "Julkaisut pisteytetään niitä vastaavien mukautettujen muotojen pisteiden yhteenlaskun summalla. Julkaisu kaapataan, jos se parantaa pisteytystä nykyisellä tai sitä paremmalla laadulla.", + "CustomFormatHelpText": "Julkaisut pisteytetään niitä vastaavien mukautettujen muotojen pisteiden yhteenlaskun summalla. {appName} tallentaa julkaisun, jos se parantaa arvosanaa nykyisellä laadulla tai parempaa.", "RemotePathMappingHostHelpText": "Sama osoite, joka on määritty etälataustyökalulle.", "AudioLanguages": "Äänen kielet", "Grabbed": "Kaapattu", @@ -25,7 +25,6 @@ "OriginalLanguage": "Alkuperäinen kieli", "ProxyResolveIpHealthCheckMessage": "Määritetyn välityspalvelimen \"{proxyHostName}\" IP-osoitteen selvitys epäonnistui.", "SetPermissionsLinuxHelpText": "Tulisiko chmod suorittaa, kun tiedostoja tuodaan/nimetään uudelleen?", - "UrlBaseHelpText": "Lisää {appName}in URL-osoitteeseen jälkiliitteen, esim. \"http://[osoite]:[portti]/[URL-perusta]\". Oletusarvo on tyhjä.", "SetPermissionsLinuxHelpTextWarning": "Jollet ole varma mitä nämä asetukset tekevät, älä muuta niitä.", "ClickToChangeLanguage": "Vaihda kieli painamalla tästä", "EnableColorImpairedModeHelpText": "Vaihtoehtoinen tyyli, joka auttaa erottamaan värikoodatut tiedot paremmin.", @@ -47,7 +46,7 @@ "AutomaticUpdatesDisabledDocker": "Automaattisia päivityksiä ei tueta suoraan käytettäessä Dockerin päivitysmekanismia. Docker-säiliö on päivitettävä {appName}in ulkopuolella tai päivitys on suoritettava komentosarjalla.", "AddListExclusionSeriesHelpText": "Estä {appName}ia lisäämästä sarjaa listoilta.", "AppUpdated": "{appName} on päivitetty", - "AuthenticationMethodHelpText": "Vaadi {appName}in käyttöön käyttäjätunnus ja salasana.", + "AuthenticationMethodHelpText": "Vaadi {appName}in käyttöön käyttäjätunnus ja salasana", "ConnectionLostToBackend": "{appName} kadotti yhteyden taustajärjestelmään ja se on käynnistettävä uudelleen.", "DeleteTag": "Poista tunniste", "AppUpdatedVersion": "{appName} on päivitetty versioon {version} ja muutosten käyttöönottamiseksi se on käynnistettävä uudelleen. ", @@ -164,7 +163,7 @@ "OnGrab": "Kun julkaisu kaapataan", "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} ei voinut lisätä tunnistetta qBittorrentiin.", "SeriesFolderFormat": "Sarjakansioiden kaava", - "TagDetails": "Tunnisteen \"{0}\" tiedot", + "TagDetails": "Tunnisteen \"{label}\" tiedot", "DownloadClientStatusSingleClientHealthCheckMessage": "Lataustyökaluja ei ole ongelmien vuoksi käytettävissä: {downloadClientNames}", "DownloadClientValidationCategoryMissing": "Kategoriaa ei ole olemassa", "EditSelectedDownloadClients": "Muokkaa valittuja lataustyökaluja", @@ -397,7 +396,7 @@ "DelayProfilesLoadError": "Virhe ladattaessa viiveprofiileja", "DeleteDownloadClient": "Poista lataustyökalu", "DeleteBackupMessageText": "Haluatko varmasti poistaa varmuuskopion \"{name}\"?", - "DeleteIndexerMessageText": "Haluatko varmasti poistaa tietolähteen \"{0}\"?", + "DeleteIndexerMessageText": "Haluatko varmasti poistaa tietolähteen '{name}'?", "DeleteRootFolderMessageText": "Haluatko varmasti poistaa juurikansion \"{path}\"?", "DeleteReleaseProfileMessageText": "Haluatko varmasti poistaa julkaisuprofiilin \"{name}\"?", "DeleteSelectedIndexers": "Poista tietoläh(de/teet)", @@ -504,7 +503,7 @@ "RefreshAndScan": "Päivitä ja tarkista", "Refresh": "Päivitä", "ReleaseProfilesLoadError": "Virhe ladattaessa julkaisuprofiileita", - "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Etälataustyökalu \"{0}\" tallentaa lataukset kohteeseen \"{1}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", + "RemotePathMappingLocalFolderMissingHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} ei voinut lisätä Label-tunnistetta {clientName}en.", "DownloadClientDelugeValidationLabelPluginInactive": "Label-tunnistelisäosa ei ole käytössä.", "AddConditionImplementation": "Lisätään ehtoa - {implementationName}", @@ -526,7 +525,7 @@ "ResetTitles": "Palauta nimet", "RestartLater": "Käynnistän uudelleen myöhemmin", "RestartReloadNote": "Huomioi: {appName} käynnistyy palautusprosessin aikana automaattisesti uudelleen.", - "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen.", + "RestartRequiredHelpTextWarning": "Käyttöönotto vaatii in uudelleenkäynnistyksen.", "Runtime": "Kesto", "Season": "Kausi", "SeasonFolder": "Kausikohtaiset kansiot", @@ -612,7 +611,7 @@ "EditSeriesModalHeader": "Muokataan - {title}", "EnableInteractiveSearch": "Käytä manuaalihakuun", "EnableRssHelpText": "Käytetään {appName}in etsiessä julkaisuja ajoitetusti RSS-synkronoinnilla.", - "EnableSslHelpText": "Käyttöönotto vaatii {appName}in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", + "EnableSslHelpText": "Käyttöönotto vaatii in uudelleenkäynnistyksen järjestelmänvavojan oikeuksilla.", "EpisodeFileRenamedTooltip": "Jaksotiedosto nimettiin uudelleen", "EpisodeInfo": "Jakson tiedot", "EpisodeFilesLoadError": "Virhe ladattaessa jaksotiedostoja", @@ -706,7 +705,7 @@ "NoTagsHaveBeenAddedYet": "Tunnisteita ei ole vielä lisätty.", "PreferProtocol": "Suosi {preferredProtocol}-protokollaa", "RemotePathMappings": "Etäsijaintien kohdistukset", - "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Etälataustyökalu \"{0}\" ilmoitti tiedostosijainniksi \"{1}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", + "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "Etälataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta sitä ei näytä olevan olemassa. Todennäköinen syy on puuttuva tai virheellinen etäsijainnin kohdistus.", "Scheduled": "Ajoitukset", "RootFolders": "Juurikansiot", "RssSyncInterval": "RSS-synkronoinnin ajoitus", @@ -817,7 +816,7 @@ "AnimeEpisodeTypeFormat": "Absoluuttinen jaksonumerointi ({format})", "AnimeEpisodeTypeDescription": "Jaksot julkaistaan absoluuttisella numeroinnilla.", "CalendarLegendEpisodeDownloadedTooltip": "Jakso on ladattu ja lajiteltu", - "BranchUpdate": "{appName}in versiopäivityksiin käytettävä kehityshaara.", + "BranchUpdate": "{appName}in versiopäivityksiin käytettävä kehityshaara", "CollapseMultipleEpisodesHelpText": "Tiivistä useat samana päivänä esitettävät jaksot.", "CalendarLegendSeriesFinaleTooltip": "Sarjan tai kauden päätösjakso", "CalendarLegendSeriesPremiereTooltip": "Sarjan tai kauden pilottijakso", @@ -841,7 +840,7 @@ "DeleteDownloadClientMessageText": "Haluatko varmasti poistaa lataustyökalun \"{name}\"?", "DeleteSelectedDownloadClients": "Poista lataustyökalu(t)", "DeleteSelectedIndexersMessageText": "Haluatko varmasti poistaa {count} valit(un/tua) tietoläh(teen/dettä)?", - "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{customFormatName}\"?", + "DeleteCustomFormatMessageText": "Haluatko varmasti poistaa mukautetun muodon \"{name}\"?", "DeleteRemotePathMapping": "Poista etäsijainnin kohdistus", "DeleteSelectedImportLists": "Poista tuontilista(t)", "DetailedProgressBar": "Yksityiskohtainen tilapalkki", @@ -900,7 +899,7 @@ "FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}", "HardlinkCopyFiles": "Hardlink/tiedostojen kopiointi", "ExternalUpdater": "{appName} on määritetty käyttämään ulkoista päivitysratkaisua.", - "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnista mihin sarjalle ja jaksolle julkaisu kuuluu, eikä sen automaattinen tuonti onnistu. Haluatko kaapata julkaisun \"{0}\"?", + "GrabReleaseUnknownSeriesOrEpisodeMessageText": "{appName} ei tunnista mihin sarjalle ja jaksolle julkaisu kuuluu, eikä sen automaattinen tuonti onnistu. Haluatko kaapata julkaisun \"{title}\"?", "FailedToUpdateSettings": "Asetusten päivitys epäonnistui", "Forums": "Keskustelualue", "ErrorLoadingPage": "Virhe ladattaessa sivua", @@ -946,7 +945,7 @@ "QualityCutoffNotMet": "Laadun katkaisutasoa ei ole saavutettu", "ProtocolHelpText": "Valitse käytettävä(t) protokolla(t) ja mitä käytetään ensisijaisesti valittaessa muutoin tasaveroisista julkaisuista.", "QualityDefinitionsLoadError": "Virhe ladattaessa laatumäärityksiä", - "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{0}\" tallentaa lataukset kohteeseen \"{1}\", mutta se ei ole kelvollinen {2}-sijainti. Tarkista lataustyökalun asetukset.", + "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{downloadClientName}\" tallentaa lataukset kohteeseen \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista lataustyökalun asetukset.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Paikallinen lataustyökalu \"{downloadClientName}\" ilmoitti tiedostosijainniksi \"{path}\", mutta se ei ole kelvollinen {osName}-sijainti. Tarkista lataustyökalun asetukset.", "RemoveDownloadsAlert": "Poistoasetukset on siirretty yllä olevan taulukon lataustyökalukohtaisiin asetuksiin.", "QualityProfile": "Laatuprofiili", @@ -1084,7 +1083,7 @@ "UtcAirDate": "UTC-esitysaika", "FileManagement": "Tiedostojen hallinta", "InteractiveImportNoEpisode": "Jokaiselle valitulle tiedostolle on valittava ainakin yksi jakso.", - "ApiKeyValidationHealthCheckMessage": "Muuta rajapinnan (API) avain ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa.", + "ApiKeyValidationHealthCheckMessage": "Muuta rajapinnan (API) avain ainakin {length} merkin pituiseksi. Voit tehdä tämän asetuksista tai muokkaamalla asetustiedostoa", "Conditions": "Ehdot", "MinimumCustomFormatScore": "Mukautetun muodon vähimmäispisteytys", "Period": "Piste", @@ -1122,7 +1121,7 @@ "RecyclingBinCleanup": "Roskakorin tyhjennys", "RecyclingBinCleanupHelpText": "Arvo \"0\" (nolla) poistaa automaattisen tyhjennyksen käytöstä.", "ReleaseSceneIndicatorAssumingScene": "Oletetuksena kohtausnumerointi.", - "ConditionUsingRegularExpressions": "Ehto vastaa säännöllisiä lausekkeita. Huomioi, että merkeillä \"\\^$.|?*+()[{\" on erityismerkityksiä ja ne on erotettava \"\\\"-merkillä.", + "ConditionUsingRegularExpressions": "Ehto vastaa säännöllisiä lausekkeita. Huomioi, että merkeillä `\\^$.|?*+()[{`on erityismerkityksiä ja ne on erotettava `\\`-merkillä", "CreateGroup": "Luo ryhmä", "Custom": "Mukautettu", "CustomFormatJson": "Mukautetun muodon JSON-koodi", @@ -1202,7 +1201,7 @@ "CountSelectedFile": "{selectedCount} tiedosto on valittu", "SingleEpisodeInvalidFormat": "Yksittäinen jakso: virheellinen kaava", "Underscore": "Alaviiva", - "AllSeriesInRootFolderHaveBeenImported": "Kaikki sijainnin {path} sisältämät sarjat on tuotu.", + "AllSeriesInRootFolderHaveBeenImported": "Kaikki sijainnin {path} sisältämät sarjat on tuotu", "AlreadyInYourLibrary": "Kohde on jo krijastossasi", "Analytics": "Analytiikka", "AuthenticationRequired": "Vaadi tunnistautuminen", @@ -1226,7 +1225,7 @@ "DestinationRelativePath": "Kohde suhteessa polkuun", "Disabled": "Ei käytössä", "Dates": "Päiväykset", - "DeleteAutoTagHelpText": "Haluatko varmasti poistaa automaattitunnisteen '\"0}\"?", + "DeleteAutoTagHelpText": "Haluatko varmasti poistaa automaattitunnisteen '{name}'?", "DeleteAutoTag": "Poista automaattitunniste", "DotNetVersion": ".NET", "DownloadClientPneumaticSettingsStrmFolder": "Strm-kansio", @@ -1396,7 +1395,6 @@ "NotificationsValidationInvalidUsernamePassword": "Virheellinen käyttäjätunnus tai salasana", "QueueFilterHasNoItems": "Mikään kohde ei vastaa valittua jonon suodatinta", "RegularExpression": "Säännöllinen lauseke", - "ReleaseProfileIndexerHelpTextWarning": "Tietyn tietolähteen käyttö julkaisuprofiileilla saattaa aiheuttaa julkaisujen kaksoiskappaleiden kaappauksia.", "ReleaseSceneIndicatorUnknownMessage": "Jakson numerointi vaihtelee, eikä julkaisu vastaa mitään tunnettua numerointia.", "DownloadClientSabnzbdValidationEnableJobFolders": "Käytä työkansioita", "EpisodeFileDeleted": "Jaksotiedosto poistettiin", @@ -1619,7 +1617,7 @@ "NotificationsPushBulletSettingsDeviceIds": "Laite-ID:t", "NotificationsKodiSettingsDisplayTime": "Näytä aika", "NotificationsSettingsWebhookUrl": "Webhook-URL-osoite", - "NotificationsSettingsUseSslHelpText": "Muodosta yhteys SSL-protokollan välityksellä.", + "NotificationsSettingsUseSslHelpText": "Muodosta yhteys sovellukseen {serviceName} SSL-protokollan välityksellä.", "NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName}-sijainti, jonka mukaisesti sarjasijainteja muutetaan kun {serviceName} näkee kirjastosijainnin eri tavalla kuin {appName} (vaatii \"Päivitä kirjasto\" -asetuksen).", "NotificationsSettingsUpdateMapPathsTo": "Kohdista sijainnit kohteeseen", "NotificationsTelegramSettingsChatIdHelpText": "Vastaanottaaksesi viestejä, sinun on aloitettava keskustelu botin kanssa tai lisättävä se ryhmääsi.", diff --git a/src/NzbDrone.Core/Localization/Core/fr.json b/src/NzbDrone.Core/Localization/Core/fr.json index 65046c53b..bfcedc8d1 100644 --- a/src/NzbDrone.Core/Localization/Core/fr.json +++ b/src/NzbDrone.Core/Localization/Core/fr.json @@ -109,13 +109,13 @@ "AutoRedownloadFailedHelpText": "Recherche automatique et tentative de téléchargement d'une version différente", "AutoTaggingLoadError": "Impossible de charger le balisage automatique", "AuthenticationRequiredWarning": "Pour empêcher l'accès à distance sans authentification, {appName} exige désormais que l'authentification soit activée. Vous pouvez éventuellement désactiver l'authentification pour les adresses locales.", - "BackupFolderHelpText": "Les chemins d'accès relatifs se trouvent dans le répertoire AppData de Sonarr", + "BackupFolderHelpText": "Les chemins d'accès relatifs se trouvent dans le répertoire AppData de {appName}", "AirDate": "Date de diffusion", "AllTitles": "Tous les titres", "AutoAdd": "Ajout automatique", "AutoTagging": "Balisage automatique", - "AutoTaggingNegateHelpText": "Si cette case est cochée, la règle de marquage automatique ne s'appliquera pas si la condition {implementationName} est remplie.", - "AutoTaggingRequiredHelpText": "Cette condition {implementationName} doit être remplie pour que la règle de marquage automatique s'applique. Dans le cas contraire, une seule correspondance {implementationName} suffit.", + "AutoTaggingNegateHelpText": "Si cette case est cochée, la règle de marquage automatique ne s'appliquera pas si cette condition {implementationName} correspond.", + "AutoTaggingRequiredHelpText": "Cette condition {implementationName} doit correspondre pour que la règle de marquage automatique s'applique. Sinon, une seule correspondance {implementationName} suffit.", "AllResultsAreHiddenByTheAppliedFilter": "Tous les résultats sont masqués par le filtre appliqué", "ApplyTagsHelpTextReplace": "Remplacer : remplace les étiquettes par les étiquettes renseignées (ne pas renseigner d'étiquette pour toutes les effacer)", "Agenda": "Agenda", @@ -150,30 +150,30 @@ "AnimeEpisodeTypeDescription": "Episodes diffusés en utilisant un numéro d'épisode absolu", "Any": "Tous", "AppUpdated": "{appName} mis à jour", - "AddListExclusionSeriesHelpText": "Empêcher les séries d'être ajoutées à Sonarr par des listes", + "AddListExclusionSeriesHelpText": "Empêcher les séries d'être ajoutées à {appName} par des listes", "AllSeriesAreHiddenByTheAppliedFilter": "Tous les résultats sont masqués par le filtre appliqué", - "AnalyseVideoFilesHelpText": "Extraire des fichiers des informations vidéo telles que la résolution, la durée d'exécution et le codec. Pour ce faire, Sonarr doit lire des parties du fichier, ce qui peut entraîner une activité élevée du disque ou du réseau pendant les analyses.", - "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs aux serveurs de Sonarr. Cela inclut des informations sur votre navigateur, les pages de l'interface Web de Sonarr que vous utilisez, les rapports d'erreurs ainsi que le système d'exploitation et la version d'exécution. Nous utiliserons ces informations pour prioriser les fonctionnalités et les corrections de bugs.", + "AnalyseVideoFilesHelpText": "Extraire des fichiers des informations vidéo telles que la résolution, la durée d'exécution et le codec. Pour ce faire, {appName} doit lire des parties du fichier, ce qui peut entraîner une activité élevée du disque ou du réseau pendant les analyses.", + "AnalyticsEnabledHelpText": "Envoyer des informations anonymes sur l'utilisation et les erreurs aux serveurs de {appName}. Cela inclut des informations sur votre navigateur, les pages de l'interface Web de {appName} que vous utilisez, les rapports d'erreurs ainsi que le système d'exploitation et la version d'exécution. Nous utiliserons ces informations pour prioriser les fonctionnalités et les corrections de bugs.", "AuthenticationMethodHelpTextWarning": "Veuillez choisir une méthode d'authentification valide", "AuthenticationRequiredHelpText": "Modifier les demandes pour lesquelles l'authentification est requise. Ne rien modifier si vous n'en comprenez pas les risques.", "AutomaticUpdatesDisabledDocker": "Les mises à jour automatiques ne sont pas directement prises en charge lors de l'utilisation du mécanisme de mise à jour de Docker. Vous devrez mettre à jour l'image du conteneur en dehors de {appName} ou utiliser un script", "BackupRetentionHelpText": "Les sauvegardes automatiques plus anciennes que la période de rétention seront nettoyées automatiquement", "QualityProfile": "Profil de qualité", - "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "Sonarr peut voir mais ne peut pas accéder à l'épisode téléchargé {path}. Probablement une erreur de permissions.", + "RemotePathMappingDownloadPermissionsEpisodeHealthCheckMessage": "{appName} peut voir mais ne peut pas accéder à l'épisode téléchargé {path}. Probablement une erreur de permissions.", "RemotePathMappingDockerFolderMissingHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement {downloadClientName} place les téléchargements dans {path}, mais ce répertoire ne semble pas exister dans le conteneur. Vérifiez vos mappages de chemins d'accès distants et les paramètres de volume du conteneur.", "BlocklistReleases": "Publications de la liste de blocage", "BindAddress": "Adresse de liaison", "BackupsLoadError": "Impossible de charger les sauvegardes", "BuiltIn": "Intégré", "BrowserReloadRequired": "Rechargement du navigateur requis", - "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score est supérieur au format personnalisé", + "BypassDelayIfAboveCustomFormatScore": "Ignorer si le score du format personnalisé est supérieur", "CheckDownloadClientForDetails": "Pour plus de détails, consultez le client de téléchargement", "ChooseAnotherFolder": "Sélectionnez un autre dossier", "BlocklistLoadError": "Impossible de charger la liste de blocage", - "BranchUpdate": "Branche à utiliser pour mettre à jour Sonarr", + "BranchUpdate": "Branche à utiliser pour mettre à jour {appName}", "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Score minimum pour le format personnalisé", "CalendarLoadError": "Impossible de charger le calendrier", - "BypassDelayIfAboveCustomFormatScoreHelpText": "Ignorer lorsque la version a un score supérieur au score minimum configuré pour le format personnalisé", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Activer le contournement lorsque la libération a un score supérieur au score minimum configuré pour le format personnalisé", "CertificateValidationHelpText": "Modifier le niveau de rigueur de la validation de la certification HTTPS. Ne pas modifier si vous ne maîtrisez pas les risques.", "Certification": "Certification", "ChangeFileDateHelpText": "Modifier la date du fichier lors de l'importation/la réanalyse", @@ -208,7 +208,7 @@ "RemotePathMappingBadDockerPathHealthCheckMessage": "Vous utilisez Docker ; le client de téléchargement {downloadClientName} place les téléchargements dans {path} mais ce n'est pas un chemin {osName} valide. Revoyez vos mappages de chemins d'accès distants et les paramètres du client de téléchargement.", "RemotePathMappingFilesLocalWrongOSPathHealthCheckMessage": "Le client de téléchargement local {downloadClientName} a signalé des fichiers dans {path}, mais il ne s'agit pas d'un chemin {osName} valide. Vérifiez les paramètres de votre client de téléchargement.", "RemotePathMappingFilesWrongOSPathHealthCheckMessage": "Le client de téléchargement distant {downloadClientName} a signalé des fichiers dans {path}, mais il ne s'agit pas d'un chemin {osName} valide. Revoyez vos mappages de chemins d'accès distants et les paramètres du client de téléchargement.", - "RemotePathMappingFolderPermissionsHealthCheckMessage": "Sonarr peut voir mais ne peut pas accéder au répertoire de téléchargement {downloadPath}. Il s'agit probablement d'une erreur de permissions.", + "RemotePathMappingFolderPermissionsHealthCheckMessage": "{appName} peut voir mais ne peut pas accéder au répertoire de téléchargement {downloadPath}. Il s'agit probablement d'une erreur de permissions.", "Path": "Chemin", "QueueIsEmpty": "La file d'attente est vide", "Warn": "Avertissement", @@ -243,9 +243,9 @@ "Edit": "Modifier", "RemoveSelectedItem": "Supprimer l'élément sélectionné", "SubtitleLanguages": "Langues des sous-titres", - "Clone": "Cloner", + "Clone": "Dupliquer", "ColonReplacementFormatHelpText": "Changer la manière dont {appName} remplace les « deux-points »", - "DefaultCase": "Casse par défaut", + "DefaultCase": "Case par défaut", "Delete": "Supprimer", "DelayProfiles": "Profils de retard", "DelayProfilesLoadError": "Impossible de charger les profils de retard", @@ -304,7 +304,7 @@ "NoIndexersFound": "Aucun indexeur n'a été trouvé", "Profiles": "Profils", "Dash": "Tiret", - "DelayProfileProtocol": "Protocole : {preferredProtocol}", + "DelayProfileProtocol": "Protocole: {preferredProtocol}", "DeleteBackupMessageText": "Voulez-vous supprimer la sauvegarde « {name} » ?", "DeleteConditionMessageText": "Voulez-vous vraiment supprimer la condition « {name} » ?", "DeleteCondition": "Supprimer la condition", @@ -322,7 +322,7 @@ "Host": "Hôte", "ICalIncludeUnmonitoredEpisodesHelpText": "Inclure les épisodes non surveillés dans le flux iCal", "RenameEpisodesHelpText": "{appName} utilisera le nom de fichier existant si le changement de nom est désactivé", - "RestartRequiredToApplyChanges": "{appName} nécessite un redémarrage pour appliquer les modifications. Voulez-vous redémarrer maintenant ?", + "RestartRequiredToApplyChanges": "{appName} nécessite un redémarrage pour appliquer les changements, voulez-vous redémarrer maintenant ?", "OrganizeRenamingDisabled": "Le renommage est désactivé, rien à renommer", "PendingChangesStayReview": "Rester et vérifier les modifications", "PendingChangesMessage": "Vous avez des modifications non sauvegardées, voulez-vous vraiment quitter cette page ?", @@ -378,7 +378,7 @@ "OneSeason": "1 saison", "Ok": "OK", "PendingChangesDiscardChanges": "Abandonner les modifications et quitter", - "PreferProtocol": "Préféré {preferredProtocol}", + "PreferProtocol": "Préférer {preferredProtocol}", "Refresh": "Rafraîchir", "PrefixedRange": "Plage préfixée", "PreferredProtocol": "Protocole préféré", @@ -451,7 +451,7 @@ "PrioritySettings": "Priorité : {priority}", "ImportExistingSeries": "Importer une série existante", "RootFolderSelectFreeSpace": "{freeSpace} Libre", - "WantMoreControlAddACustomFormat": "Vous voulez plus de contrôle sur les téléchargements préférés ? Ajouter un [Format Personnalisé](/settings/customformats)", + "WantMoreControlAddACustomFormat": "Vous souhaitez avoir plus de contrôle sur les téléchargements préférés ? Ajoutez un [Format personnalisé](/settings/customformats)", "RemoveSelectedItemsQueueMessageText": "Voulez-vous vraiment supprimer {selectedCount} éléments de la file d'attente ?", "UpdateAll": "Tout actualiser", "EnableSslHelpText": "Nécessite un redémarrage en tant qu'administrateur pour être effectif", @@ -576,7 +576,7 @@ "MaximumSize": "Taille maximum", "Mechanism": "Mécanisme", "MediaInfo": "Informations médias", - "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages supporte un suffixe `:EN+DE` vous permettant de filtrer les langues incluses dans le nom de fichier. Utilisez `-DE` pour exclure des langues spécifiques. L'ajout de `+` (par exemple `:EN+`) affichera `[EN]`/`[EN+--]`/`[--]` en fonction des langues exclues. Par exemple `{MediaInfo Full:EN+DE}`.", + "MediaInfoFootNote": "MediaInfo Full/AudioLanguages/SubtitleLanguages supporte un suffixe `:EN+DE` vous permettant de filtrer les langues incluses dans le nom de fichier. Utilisez `-DE` pour exclure des langues spécifiques. En ajoutant `+` (par exemple `:EN+`), vous obtiendrez `[EN]`/`[EN+--]`/`[--]` en fonction des langues exclues. Par exemple `{MediaInfo Full:EN+DE}`.", "MetadataProvidedBy": "Les métadonnées sont fournies par {provider}", "MetadataSettings": "Paramètres des métadonnées", "MetadataSettingsSeriesSummary": "Créez des fichiers de métadonnées lorsque les épisodes sont importés ou que les séries sont actualisées", @@ -601,14 +601,14 @@ "MustNotContainHelpText": "La version sera rejetée si elle contient un ou plusieurs termes (insensible à la casse)", "NamingSettings": "Paramètres de dénomination", "Negate": "Nier", - "NegateHelpText": "Si cette case est cochée, le format personnalisé ne s'appliquera pas si cette condition {implementationName} correspond.", + "NegateHelpText": "Si coché, le format personnalisé ne s'appliquera pas si cette condition {implementationName} correspond.", "Negated": "Nier", "Network": "Réseau", "Never": "Jamais", "New": "Nouveau", "NextExecution": "Prochaine exécution", "NoChange": "Pas de changement", - "NoDelay": "Sans délais", + "NoDelay": "Pas de délai", "NoEpisodeHistory": "Pas d'historique des épisodes", "NoEpisodesInThisSeason": "Aucun épisode dans cette saison", "NoEventsFound": "Aucun événement trouvé", @@ -621,7 +621,7 @@ "Organize": "Organiser", "OrganizeLoadError": "Erreur lors du chargement des aperçus", "OrganizeModalHeader": "Organiser et renommer", - "OrganizeModalHeaderSeason": "Organiser et renommer – {saison}", + "OrganizeModalHeaderSeason": "Organiser et renommer – {season}", "OrganizeSelectedSeriesModalAlert": "Astuce : Pour prévisualiser un changement de nom, sélectionnez \"Annuler\", puis sélectionnez n'importe quel titre de série et utilisez cette icône :", "OrganizeSelectedSeriesModalConfirmation": "Voulez-vous vraiment organiser tous les fichiers des {count} séries sélectionnées ?", "OrganizeSelectedSeriesModalHeader": "Organiser les séries sélectionnées", @@ -651,10 +651,9 @@ "RelativePath": "Chemin relatif", "Release": "Version", "ReleaseGroup": "Groupe de versions", - "ReleaseGroups": "Groupes de versions", + "ReleaseGroups": "Groupes de version", "ReleaseHash": "Somme de contrôle de la version", "ReleaseProfile": "Profil de version", - "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut conduire à la saisie de versions en double", "ReleaseProfiles": "Profils de version", "ReleaseProfilesLoadError": "Impossible de charger les profils de version", "RemotePathMappingGenericPermissionsHealthCheckMessage": "Le client de téléchargement {downloadClientName} place les téléchargements dans {path} mais {appName} ne peut pas voir ce répertoire. Vous devrez peut-être ajuster les autorisations du dossier.", @@ -934,7 +933,7 @@ "OnGrab": "À saisir", "OnlyForBulkSeasonReleases": "Uniquement pour les versions de saison en masse", "RegularExpressionsCanBeTested": "Les expressions régulières peuvent être testées [ici]({url}).", - "ReleaseProfileIndexerHelpText": "Spécifiez à quel indexeur le profil s'applique", + "ReleaseProfileIndexerHelpText": "Spécifier l'indexeur auquel le profil s'applique", "RemotePathMappings": "Mappages de chemins distants", "RescanAfterRefreshHelpTextWarning": "{appName} ne détectera pas automatiquement les modifications apportées aux fichiers lorsqu'il n'est pas défini sur 'Toujours'", "SingleEpisode": "Épisode unique", @@ -988,13 +987,13 @@ "Min": "Min", "MinimumAge": "Âge minimum", "MinimumAgeHelpText": "Usenet uniquement : âge minimum en minutes des NZB avant leur saisie. Utilisez-le pour donner aux nouvelles versions le temps de se propager à votre fournisseur Usenet.", - "MinutesSixty": "60 Minutes : {sixty}", + "MinutesSixty": "60 Minutes : {sixty}", "MonitoredOnly": "Surveillé uniquement", "MoveSeriesFoldersDontMoveFiles": "Non, je déplacerai les fichiers moi-même", "MoveSeriesFoldersMoveFiles": "Oui, déplacez les fichiers", "MoveSeriesFoldersToNewPath": "Souhaitez-vous déplacer les fichiers de la série de « {originalPath} » vers « {destinationPath} » ?", - "MoveSeriesFoldersToRootFolder": "Souhaitez-vous déplacer les dossiers de la série vers « {DestinationRootFolder} » ?", - "MustContainHelpText": "Le communiqué doit contenir au moins un de ces termes (insensible à la casse)", + "MoveSeriesFoldersToRootFolder": "Souhaitez-vous déplacer les dossiers de la série vers « {destinationRootFolder} » ?", + "MustContainHelpText": "La version doit contenir au moins l'un des termes suivants (insensible à la casse)", "MustNotContain": "Ne doit pas contenir", "NamingSettingsLoadError": "Impossible de charger les paramètres de dénomination", "NoEpisodeInformation": "Aucune information sur l'épisode n'est disponible.", @@ -1054,7 +1053,7 @@ "Level": "Niveau", "LibraryImport": "Importer biblio.", "ListExclusionsLoadError": "Impossible de charger les exclusions de liste", - "ListQualityProfileHelpText": "Les éléments de la liste des profils de qualité seront ajoutés avec", + "ListQualityProfileHelpText": "Les éléments de la liste du profil de qualité seront ajoutés avec", "ListTagsHelpText": "Balises qui seront ajoutées lors de l'importation à partir de cette liste", "LocalAirDate": "Date de diffusion locale", "Location": "Emplacement", @@ -1067,7 +1066,7 @@ "MultiEpisode": "Multi-épisode", "MultiEpisodeInvalidFormat": "Épisode multiple : format invalide", "NoEpisodeOverview": "Aucun aperçu des épisodes", - "OneMinute": "1 Minute", + "OneMinute": "1 minute", "OriginalLanguage": "Langue originale", "Port": "Port", "PreferTorrent": "Préféré Torrent", @@ -1103,12 +1102,12 @@ "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "Activer la gestion des téléchargements terminés si possible (multi-ordinateur non pris en charge)", "ImportMechanismHandlingDisabledHealthCheckMessage": "Activer la gestion des téléchargements terminés", "ImportUsingScript": "Importer à l'aide d'un script", - "IncludeHealthWarnings": "Inclure des avertissements de santé", + "IncludeHealthWarnings": "Inclure les avertissements de santé", "Indexer": "Indexeur", "LibraryImportTipsSeriesUseRootFolder": "Pointez {appName} vers le dossier contenant toutes vos émissions de télévision, pas une en particulier. par exemple. \"`{goodFolderExample}`\" et non \"`{badFolderExample}`\". De plus, chaque série doit se trouver dans son propre dossier dans le dossier racine/bibliothèque.", "Links": "Liens", "ListOptionsLoadError": "Impossible de charger les options de la liste", - "ListRootFolderHelpText": "Les éléments de la liste du dossier racine seront ajoutés à", + "ListRootFolderHelpText": "Les éléments de la liste du dossier racine seront ajoutés à la liste des dossiers racine", "MinutesThirty": "30 Minutes : {thirty}", "Missing": "Manquant", "MissingEpisodes": "Épisodes manquants", @@ -1152,11 +1151,11 @@ "RemovedFromTaskQueue": "Supprimé de la file d'attente des tâches", "RemovedSeriesSingleRemovedHealthCheckMessage": "La série {series} a été supprimée de TheTVDB", "Reorder": "Réorganiser", - "Repack": "Remballer", + "Repack": "Repack", "RequiredHelpText": "Cette condition {implementationName} doit correspondre pour que le format personnalisé s'applique. Sinon, une seule correspondance {implementationName} suffit.", "RescanSeriesFolderAfterRefresh": "Réanalyser le dossier de la série après l'actualisation", "ResetAPIKey": "Réinitialiser la clé API", - "RestartRequiredWindowsService": "Selon l'utilisateur qui exécute le service {appName}, vous devrez peut-être redémarrer {appName} en tant qu'administrateur une fois avant que le service ne démarre automatiquement.", + "RestartRequiredWindowsService": "En fonction de l'utilisateur qui exécute le service {appName}, vous devrez peut-être redémarrer {appName} en tant qu'administrateur une fois avant que le service ne démarre automatiquement.", "Restore": "Restaurer", "RestoreBackup": "Restaurer la sauvegarde", "RestrictionsLoadError": "Impossible de charger les restrictions", @@ -1174,7 +1173,7 @@ "StartImport": "Démarrer l'importation", "Started": "Démarré", "StartupDirectory": "Répertoire de démarrage", - "SupportedAutoTaggingProperties": "{appName} prend en charge les propriétés suivantes pour les règles de marquage automatique", + "SupportedAutoTaggingProperties": "{appName} prend en charge les propriétés suivantes pour les règles d'étiquetage automatique", "ToggleUnmonitoredToMonitored": "Non surveillé, cliquez pour surveiller", "Torrents": "Torrents", "Total": "Total", @@ -1195,7 +1194,7 @@ "InstanceName": "Nom de l'instance", "InteractiveImportLoadError": "Impossible de charger les éléments d'importation manuelle", "InteractiveImportNoEpisode": "Un ou plusieurs épisodes doivent être choisis pour chaque fichier sélectionné", - "MappedNetworkDrivesWindowsService": "Les lecteurs réseau mappés ne sont pas disponibles lors de l'exécution en tant que service Windows, consultez la [FAQ](https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote -serveur) pour plus d'informations.", + "MappedNetworkDrivesWindowsService": "Les lecteurs réseau mappés ne sont pas disponibles lors de l'exécution en tant que service Windows, consultez la [FAQ]({url}) pour plus d'informations.", "SelectReleaseGroup": "Sélectionnez un groupe de versions", "Tomorrow": "Demain", "OverrideGrabNoSeries": "La série doit être sélectionnée", @@ -1206,7 +1205,7 @@ "ReleaseSceneIndicatorAssumingScene": "En supposant la numérotation des scènes.", "ReleaseSceneIndicatorAssumingTvdb": "En supposant la numérotation TVDB.", "ReleaseSceneIndicatorMappedNotRequested": "L'épisode mappé n'a pas été demandé dans cette recherche.", - "SearchForQuery": "Rechercher {requête}", + "SearchForQuery": "Rechercher {query}", "View": "Vues", "HardlinkCopyFiles": "Lien physique/Copie de fichiers", "Health": "Santé", @@ -1250,8 +1249,8 @@ "Debug": "Déboguer", "DelayProfileSeriesTagsHelpText": "S'applique aux séries avec au moins une balise correspondante", "DelayingDownloadUntil": "Retarder le téléchargement jusqu'au {date} à {time}", - "DeletedReasonManual": "Le fichier a été supprimé via l'interface utilisateur", - "DeleteRemotePathMapping": "Supprimer le mappage de chemin distant", + "DeletedReasonManual": "Le fichier a été supprimé à l'aide de {appName}, soit manuellement, soit par un autre outil via l'API.", + "DeleteRemotePathMapping": "Supprimer la correspondance de chemin distant", "DestinationPath": "Chemin de destination", "DestinationRelativePath": "Chemin relatif de destination", "DownloadClientRootFolderHealthCheckMessage": "Le client de téléchargement {downloadClientName} place les téléchargements dans le dossier racine {rootFolderPath}. Vous ne devez pas télécharger vers un dossier racine.", @@ -1263,7 +1262,7 @@ "EditCustomFormat": "Modifier le format personnalisé", "Downloading": "Téléchargement", "EditListExclusion": "Modifier l'exclusion de liste", - "EditMetadata": "Modifier les métadonnées {metadataType}", + "EditMetadata": "Modifier {metadataType} Métadonnée", "EnableAutomaticAdd": "Activer l'ajout automatique", "EpisodeFileDeleted": "Fichier de l'épisode supprimé", "EpisodeFileDeletedTooltip": "Fichier de l'épisode supprimé", @@ -1277,13 +1276,13 @@ "Component": "Composant", "Condition": "Condition", "Connections": "Connexions", - "ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs multimédias et scripts personnalisés", + "ConnectSettingsSummary": "Notifications, connexions aux serveurs/lecteurs de médias et scripts personnalisés", "CopyToClipboard": "Copier dans le presse-papier", "CreateEmptySeriesFolders": "Créer des dossiers de séries vides", "Custom": "Customisé", "CopyUsingHardlinksSeriesHelpText": "Les liens physiques permettent à {appName} d'importer des torrents dans le dossier de la série sans prendre d'espace disque supplémentaire ni copier l'intégralité du contenu du fichier. Les liens physiques ne fonctionneront que si la source et la destination sont sur le même volume", - "CustomFormatsSettingsSummary": "Paramètres de formats personnalisés", - "CustomFormatsSettings": "Paramètres de formats personnalisés", + "CustomFormatsSettingsSummary": "Formats et paramètres personnalisés", + "CustomFormatsSettings": "Paramètre des formats personnalisés", "DefaultDelayProfileSeries": "Il s'agit du profil par défaut. Cela s'applique à toutes les séries qui n'ont pas de profil explicite.", "DeleteDownloadClient": "Supprimer le client de téléchargement", "DeleteEmptyFolders": "Supprimer les dossiers vides", @@ -1292,7 +1291,7 @@ "DeleteImportListExclusionMessageText": "Êtes-vous sûr de vouloir supprimer cette exclusion de la liste d'importation ?", "DeleteQualityProfile": "Supprimer le profil de qualité", "DeleteReleaseProfile": "Supprimer le profil de version", - "DeleteRemotePathMappingMessageText": "Êtes-vous sûr de vouloir supprimer ce mappage de chemin distant ?", + "DeleteRemotePathMappingMessageText": "Êtes-vous sûr de vouloir supprimer cette correspondance de chemin distant ?", "DoNotPrefer": "Ne préfère pas", "DoNotUpgradeAutomatically": "Ne pas mettre à niveau automatiquement", "DownloadClient": "Client de téléchargement", @@ -1340,23 +1339,23 @@ "Database": "Base de données", "Dates": "Dates", "CustomFormatJson": "Format personnalisé JSON", - "DelayMinutes": "{delay} Minutes", + "DelayMinutes": "{delay} minutes", "DelayProfile": "Profil de retard", "DeleteDelayProfile": "Supprimer le profil de retard", "DeleteDelayProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de retard ?", "DeleteEpisodeFile": "Supprimer le fichier de l'épisode", - "DeleteEpisodeFileMessage": "Supprimer le fichier de l'épisode ?", + "DeleteEpisodeFileMessage": "Supprimer le fichier de l'épisode '{path}'?", "DeleteEpisodeFromDisk": "Supprimer l'épisode du disque", - "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer cette exclusion de la liste d'importation ?", + "DeleteImportListMessageText": "Êtes-vous sûr de vouloir supprimer la liste « {name} » ?", "DeleteSelectedEpisodeFiles": "Supprimer les fichiers d'épisode sélectionnés", "DeleteSelectedEpisodeFilesHelpText": "Êtes-vous sûr de vouloir supprimer les fichiers d'épisode sélectionnés ?", - "DeleteSpecificationHelpText": "Êtes-vous sûr de vouloir supprimer la spécification « {name} » ?", + "DeleteSpecificationHelpText": "Êtes-vous sûr de vouloir supprimer la spécification '{name}' ?", "DeleteTag": "Supprimer l'étiquette", "DownloadClientStatusSingleClientHealthCheckMessage": "Clients de téléchargement indisponibles en raison d'échecs : {downloadClientNames}", "DownloadClientStatusAllClientHealthCheckMessage": "Tous les clients de téléchargement sont indisponibles en raison d'échecs", "DownloadClientsLoadError": "Impossible de charger les clients de téléchargement", "DownloadPropersAndRepacks": "Propriétés et reconditionnements", - "DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins distants", + "DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins d'accès à distance", "DownloadPropersAndRepacksHelpText": "S'il faut ou non mettre à niveau automatiquement vers Propers/Repacks", "DownloadPropersAndRepacksHelpTextWarning": "Utilisez des formats personnalisés pour les mises à niveau automatiques vers Propers/Repacks", "DownloadPropersAndRepacksHelpTextCustomFormat": "Utilisez « Ne pas préférer » pour trier par score de format personnalisé sur Propers/Repacks", @@ -1390,7 +1389,7 @@ "CustomFilters": "Filtres personnalisés", "CustomFormat": "Format personnalisé", "CustomFormatHelpText": "{appName} attribue un score pour chaque release en additionnant les scores des formats personnalisés correspondants. Si une nouvelle release permet d'améliorer le score, pour une qualité identique ou supérieure, alors {appName} la téléchargera.", - "CustomFormatUnknownCondition": "Condition de format personnalisé inconnue '{implémentation}'", + "CustomFormatUnknownCondition": "Condition de format personnalisé inconnue '{implementation}'", "CustomFormatUnknownConditionOption": "Option inconnue '{key}' pour la condition '{implementation}'", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "Le client de téléchargement {downloadClientName} est configuré pour supprimer les téléchargements terminés. Cela peut entraîner la suppression des téléchargements de votre client avant que {appName} puisse les importer.", "DownloadFailed": "Échec du téléchargement", @@ -1401,7 +1400,7 @@ "EnableHelpText": "Activer la création de fichiers de métadonnées pour ce type de métadonnées", "EnableInteractiveSearchHelpText": "Sera utilisé lorsque la recherche interactive est utilisée", "EnableInteractiveSearchHelpTextWarning": "La recherche n'est pas prise en charge avec cet indexeur", - "EnableProfileHelpText": "Cochez pour activer le profil de version", + "EnableProfileHelpText": "Vérifier pour activer le profil de version", "EnableRss": "Activer RSS", "Ended": "Terminé", "EndedOnly": "Terminé seulement", @@ -1416,7 +1415,7 @@ "EpisodesLoadError": "Impossible de charger les épisodes", "Files": "Fichiers", "Continuing": "Continuer", - "Donate": "Faire un don", + "Donate": "Donation", "EditConditionImplementation": "Modifier la condition – {implementationName}", "EditConnectionImplementation": "Modifier la connexion - {implementationName}", "EditImportListImplementation": "Modifier la liste d'importation - {implementationName}", @@ -1444,15 +1443,15 @@ "DeleteEmptySeriesFoldersHelpText": "Supprimez les dossiers de séries et de saisons vides lors de l'analyse du disque et lorsque les fichiers d'épisode sont supprimés", "DeleteEpisodesFiles": "Supprimer {episodeFileCount} fichiers d'épisode", "DeleteEpisodesFilesHelpText": "Supprimer les fichiers d'épisode et le dossier de série", - "DeleteQualityProfileMessageText": "Êtes-vous sûr de vouloir supprimer le profil de qualité « {name} » ?", - "DeleteReleaseProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de version « {name} » ?", + "DeleteQualityProfileMessageText": "Êtes-vous sûr de vouloir supprimer le profil de qualité \"{name}\" ?", + "DeleteReleaseProfileMessageText": "Êtes-vous sûr de vouloir supprimer ce profil de version '{name}' ?", "DeleteSelectedSeries": "Supprimer la série sélectionnée", "DeleteSeriesFolder": "Supprimer le dossier de série", "DeleteSeriesFolderCountConfirmation": "Voulez-vous vraiment supprimer {count} séries sélectionnées ?", "DeleteSeriesFolderCountWithFilesConfirmation": "Voulez-vous vraiment supprimer {count} séries sélectionnées et tous les contenus ?", - "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} fichiers d'épisode totalisant {taille}", + "DeleteSeriesFolderEpisodeCount": "{episodeFileCount} fichiers d'épisode totalisant {size}", "DeleteSeriesFoldersHelpText": "Supprimez les dossiers de séries et tout leur contenu", - "DeleteSeriesModalHeader": "Supprimer - {titre}", + "DeleteSeriesModalHeader": "Supprimer - {title}", "DeletedReasonUpgrade": "Le fichier a été supprimé pour importer une mise à niveau", "DeletedSeriesDescription": "La série a été supprimée de TheTVDB", "DetailedProgressBar": "Barre de progression détaillée", @@ -1464,7 +1463,7 @@ "DownloadClientSettings": "Télécharger les paramètres client", "EditRestriction": "Modifier la restriction", "EditSelectedSeries": "Modifier la série sélectionnée", - "EditSeriesModalHeader": "Modifier - {titre}", + "EditSeriesModalHeader": "Modifier - {title}", "Enable": "Activer", "Error": "Erreur", "ErrorLoadingContents": "Erreur lors du chargement du contenu", @@ -1486,13 +1485,13 @@ "IndexerValidationCloudFlareCaptchaRequired": "Site protégé par le CAPTCHA CloudFlare. Un jeton CAPTCHA valide est nécessaire.", "IndexerValidationTestAbortedDueToError": "Le test a été abandonné à cause d'un erreur : {exceptionMessage}", "TorrentBlackholeSaveMagnetFilesReadOnly": "Lecture seule", - "DownloadClientFloodSettingsPostImportTagsHelpText": "Ajouter les étiquettes après qu'un téléchargement est importé.", + "DownloadClientFloodSettingsPostImportTagsHelpText": "Ajoute des balises après l'importation d'un téléchargement.", "DownloadClientFreeboxSettingsAppId": "ID de l'application", - "DownloadClientFreeboxSettingsAppIdHelpText": "L'ID de l'application donné lors de la création de l'accès à l'API Freebox (c'est-à-dire « app_id »)", - "DownloadClientNzbgetSettingsAddPausedHelpText": "Cette option exige au moins la version 16.0 de NzbGet", - "DownloadStationStatusExtracting": "Extraction : {progress} %", - "IndexerHDBitsSettingsCodecsHelpText": "Si non renseigné, toutes les options sont utilisées.", - "IndexerHDBitsSettingsMediumsHelpText": "Si non renseigné, toutes les options sont utilisées.", + "DownloadClientFreeboxSettingsAppIdHelpText": "L'ID de l'application donné lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_id')", + "DownloadClientNzbgetSettingsAddPausedHelpText": "Cette option nécessite au moins la version 16.0 de NzbGet", + "DownloadStationStatusExtracting": "Extraction : {progress}%", + "IndexerHDBitsSettingsCodecsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", + "IndexerHDBitsSettingsMediumsHelpText": "Si elle n'est pas spécifiée, toutes les options sont utilisées.", "IndexerSettingsAdditionalParametersNyaa": "Paramètres supplémentaires", "IndexerSettingsAnimeCategories": "Catégories Anime", "IndexerSettingsApiUrl": "URL de l'API", @@ -1506,25 +1505,25 @@ "IndexerValidationInvalidApiKey": "Clé API invalide", "IndexerValidationUnableToConnect": "Impossible de se connecter à l'indexeur : {exceptionMessage}. Vérifiez le journal pour plus de détails sur cette erreur", "IndexerValidationRequestLimitReached": "Limite de requêtes atteinte : {exceptionMessage}", - "IndexerValidationUnableToConnectHttpError": "Impossible de se connecter à l'indexeur, vérifiez vos paramètres DNS est assurez-vous que l'IPv6 fonctionne ou est désactivé.", - "TorrentBlackholeSaveMagnetFiles": "Sauvegarder les fichiers Magnet", + "IndexerValidationUnableToConnectHttpError": "Impossible de se connecter à l'indexeur, vérifiez vos paramètres DNS est assurez-vous que l'IPv6 fonctionne ou est désactivé. {exceptionMessage}.", + "TorrentBlackholeSaveMagnetFiles": "Enregistrer les fichiers magnétiques", "Category": "Catégorie", - "Destination": "Destination", - "Directory": "Répertoire", - "DownloadClientDelugeSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL du JSON de Deluge, voir {url}", - "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} n'a pas pu ajouter les étiquettes à {clientName}.", + "Destination": "Cible", + "Directory": "Dossier", + "DownloadClientDelugeSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL json du déluge, voir {url}", + "DownloadClientDelugeValidationLabelPluginFailureDetail": "{appName} n'a pas pu ajouter le libellé à {clientName}.", "DownloadClientDelugeTorrentStateError": "Deluge signale une erreur", - "DownloadClientDelugeValidationLabelPluginFailure": "La configuration des étiquettes a échoué", + "DownloadClientDelugeValidationLabelPluginFailure": "La configuration de l'étiquette a échoué", "DownloadClientDownloadStationValidationFolderMissing": "Le dossier n'existe pas", - "DownloadClientDownloadStationValidationNoDefaultDestination": "Aucune destination par défaut", + "DownloadClientDownloadStationValidationNoDefaultDestination": "Pas de destination par défaut", "DownloadClientDownloadStationValidationSharedFolderMissing": "Le dossier partagé n'existe pas", - "DownloadClientFloodSettingsAdditionalTags": "Étiquettes supplémentaires", - "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Diskstation n'a pas de dossier partagé avec le nom « {sharedFolder} », êtes-vous sûr de l'avoir correctement indiqué ?", - "DownloadClientFreeboxApiError": "L'API Freebox a retourné l'erreur : {errorDescription}", + "DownloadClientFloodSettingsAdditionalTags": "Étiquette supplémentaire", + "DownloadClientDownloadStationValidationSharedFolderMissingDetail": "Le poste de travail n'a pas de dossier partagé portant le nom '{sharedFolder}', êtes-vous sûr de l'avoir spécifié correctement ?", + "DownloadClientFreeboxApiError": "L'API Freebox a renvoyé une erreur : {errorDescription}", "DownloadClientFreeboxNotLoggedIn": "Non connecté", - "DownloadClientFreeboxSettingsHostHelpText": "Nom de l'hôte ou adresse IP de l'hôte de la Freebox, par défaut à « {url} » (ne fonctionne que si sur le même réseau)", - "DownloadClientFreeboxUnableToReachFreeboxApi": "Impossible de contacter l'API Freebox. Vérifiez le paramètre « URL de l'API » pour l'URL de base et la version.", - "DownloadClientVuzeValidationErrorVersion": "Version du protocole non pris en charge, utilisez Vuze 5.0.0.0 ou version ultérieure avec le plugin Vuze Web Remote.", + "DownloadClientFreeboxSettingsHostHelpText": "Nom d'hôte ou adresse IP de la Freebox, par défaut '{url}' (ne fonctionnera que si elle est sur le même réseau)", + "DownloadClientFreeboxUnableToReachFreeboxApi": "Impossible d'accéder à l'API Freebox. Vérifiez le paramètre 'API URL' pour l'URL de base et la version.", + "DownloadClientVuzeValidationErrorVersion": "Version du protocole non prise en charge, utilisez Vuze 5.0.0.0 ou une version plus récente avec le plugin Vuze Web Remote.", "IndexerHDBitsSettingsCodecs": "Codecs", "IndexerHDBitsSettingsCategories": "Catégories", "IndexerSettingsAdditionalParameters": "Paramètres supplémentaires", @@ -1532,15 +1531,15 @@ "IndexerValidationFeedNotSupported": "Le flux de l'indexeur n'est pas pris en charge : {exceptionMessage}", "IndexerValidationUnableToConnectInvalidCredentials": "Impossible de se connecter à l'indexeur, identifiants invalides. {exceptionMessage}.", "IndexerHDBitsSettingsCategoriesHelpText": "Si non renseigné, toutes les options sont utilisées.", - "IndexerSettingsSeedTimeHelpText": "Le temps qu'un torrent doit être seedé avant d'arrêter, laissez vide pour utiliser la valeur du client de téléchargement par défaut", - "DownloadClientFloodSettingsUrlBaseHelpText": "Ajoute un préfixe à l'API Flood, tel que {url}", + "IndexerSettingsSeedTimeHelpText": "Durée pendant laquelle un torrent doit être envoyé avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement", + "DownloadClientFloodSettingsUrlBaseHelpText": "Ajoute d'un préfixe à l'API Flood, tel que {url}", "DownloadClientFreeboxAuthenticationError": "L'authentification à l'API Freebox a échoué. Raison : {errorDescription}", - "DownloadClientFreeboxSettingsApiUrl": "URL de l'API", - "DownloadClientFreeboxSettingsApiUrlHelpText": "Définissez l'URL de base de l'API Freebox avec la version de l'API, par ex. « {url} » est par défaut à « {defaultApiUrl} »", - "DownloadClientFreeboxSettingsAppToken": "Jeton de l'application", - "DownloadClientFreeboxSettingsAppTokenHelpText": "Le jeton de l'application récupéré lors de la création de l'accès à l'API Freebox (c'est-à-dire « app_token »)", - "DownloadClientFreeboxSettingsPortHelpText": "Port utilisé pour accéder à l'interface de la Freebox, par défaut à « {port} »", - "DownloadClientFreeboxUnableToReachFreebox": "Impossible de contacter l'API Freebox. Vérifiez les paramètres « Hôte », « Port » ou « Utiliser SSL ». (Erreur : {exceptionMessage})", + "DownloadClientFreeboxSettingsApiUrl": "URL DE L'API", + "DownloadClientFreeboxSettingsApiUrlHelpText": "Définir l'URL de base de l'API Freebox avec la version de l'API, par exemple '{url}', par défaut '{defaultApiUrl}'", + "DownloadClientFreeboxSettingsAppToken": "Jeton d'application", + "DownloadClientFreeboxSettingsAppTokenHelpText": "Le jeton de l'application récupéré lors de la création de l'accès à l'API Freebox (c'est-à-dire 'app_token')", + "DownloadClientFreeboxSettingsPortHelpText": "Port utilisé pour accéder à l'interface de la Freebox, la valeur par défaut est '{port}'", + "DownloadClientFreeboxUnableToReachFreebox": "Impossible d'accéder à l'API Freebox. Vérifiez les paramètres 'Host', 'Port' ou 'Use SSL'. (Erreur : {exceptionMessage})", "MonitorAllSeasons": "Toutes les saisons", "MonitorAllSeasonsDescription": "Surveiller automatiquement toutes les nouvelles saisons", "MonitorLastSeason": "Dernière saison", @@ -1555,81 +1554,81 @@ "PasswordConfirmation": "Confirmation du mot de passe", "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Confirmer le nouveau mot de passe", "MonitorNoNewSeasons": "Aucune nouvelle saison", - "DownloadClientFloodSettingsTagsHelpText": "Étiquettes initiales d'un téléchargement. Pour être reconnu, un téléchargement doit avoir toutes les étiquettes initiales. Cela évite les conflits avec des téléchargements non liés.", + "DownloadClientFloodSettingsTagsHelpText": "Étiquettes initiales d'un téléchargement. Pour être reconnu, un téléchargement doit avoir toutes les étiquettes initiales. Cela permet d'éviter les conflits avec des téléchargements non apparentés.", "DownloadClientQbittorrentValidationCategoryAddFailure": "La configuration de la catégorie a échoué", - "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Les catégories ne sont pas prises en charge avant la version 3.3.0 de qBittorrent. Veuillez effectuer une mise à niveau ou réessayez avec une catégorie vide.", - "DownloadClientRTorrentProviderMessage": "rTorrent ne mettra pas en pause les torrents lorsqu'ils atteindront les critères de partage. {appName} se chargera de la suppression automatique des torrents en fonction des critères de partage actuels dans Paramètres -> Indexeurs uniquement lorsque la suppression des téléchargements terminés est activée. Après l'importation, il définira également {importedView} en tant que vue rTorrent, qui peut être utilisée dans les scripts rTorrent pour personnaliser le comportement.", - "DownloadClientSettingsCategorySubFolderHelpText": "L'ajout d'une catégorie spécifique à {appName} évite les conflits avec des téléchargements non liés à {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée. Cela crée un sous-répertoire [catégorie] dans le répertoire de sortie.", + "DownloadClientQbittorrentValidationCategoryUnsupportedDetail": "Les catégories ne sont pas prises en charge avant la version 3.3.0 de qBittorrent. Veuillez effectuer une mise à niveau ou réessayer avec une catégorie vide.", + "DownloadClientRTorrentProviderMessage": "rTorrent ne mettra pas les torrents en pause lorsqu'ils répondent aux critères d'ensemencement. {appName} traitera la suppression automatique des torrents en fonction des critères d'ensemencement actuels dans Paramètres->Indexeurs uniquement lorsque l'option Supprimer terminé est activée. Après l'importation, il définira également {importedView} comme une vue rTorrent, qui peut être utilisée dans les scripts rTorrent pour personnaliser le comportement.", + "DownloadClientSettingsCategorySubFolderHelpText": "L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée. Crée un sous-répertoire [catégorie] dans le répertoire de sortie.", "IndexerValidationQuerySeasonEpisodesNotSupported": "L'indexeur ne prend pas en charge la requête actuelle. Vérifiez si les catégories et/ou la recherche de saisons/épisodes sont prises en charge. Consultez le journal pour plus de détails.", "MonitorNewItems": "Surveiller les nouveaux éléments", "UsenetBlackholeNzbFolder": "Dossier Nzb", - "IndexerSettingsApiPath": "Chemin de l'API", - "IndexerSettingsSeedTime": "Temps de partage", - "IndexerSettingsSeedRatio": "Ratio de partage", - "IndexerSettingsSeedRatioHelpText": "Le ratio que doit atteindre un torrent avant de s'arrêter, laisser vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles de l'indexeur", + "IndexerSettingsApiPath": "Chemin d'accès à l'API", + "IndexerSettingsSeedTime": "Temps d'envoie", + "IndexerSettingsSeedRatio": "Ratio d'envoie", + "IndexerSettingsSeedRatioHelpText": "Le ratio qu'un torrent doit atteindre avant de s'arrêter, vide utilise la valeur par défaut du client de téléchargement. Le ratio doit être d'au moins 1.0 et suivre les règles des indexeurs", "IndexerValidationNoRssFeedQueryAvailable": "Aucune requête de flux RSS disponible. Cela peut être un problème avec l'indexeur ou vos paramètres de catégorie de l'indexeur.", "IndexerValidationSearchParametersNotSupported": "L'indexeur ne prend pas en charge les paramètres de recherche requis", "IndexerValidationUnableToConnectResolutionFailure": "Impossible de se connecter à l'indexeur : échec de la connexion. Vérifiez votre connexion au serveur de l'indexeur et le DNS. {exceptionMessage}.", - "TorrentBlackhole": "Torrent Blackhole", + "TorrentBlackhole": "Trou noir des torrents", "UseSsl": "Utiliser SSL", "UsenetBlackhole": "Usenet Blackhole", - "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Vous devez vous connecter à votre Diskstation en tant que {username} et le configurer manuellement dans les paramètres de DownloadStation sous BT/HTTP/FTP/NZB -> Emplacement.", - "DownloadClientFloodSettingsPostImportTags": "Étiquettes après importation", - "DownloadClientFloodSettingsRemovalInfo": "{appName} se chargera de la suppression automatique des torrents en fonction des critères de partage actuels dans Paramètres -> Indexeurs", - "DownloadClientFloodSettingsStartOnAdd": "Commencer lors de l'ajout", - "DownloadClientNzbVortexMultipleFilesMessage": "Le téléchargement contient plusieurs fichiers et n'est pas dans un dossier de tâche : {outputPath}", - "DownloadClientNzbgetValidationKeepHistoryOverMax": "Le paramètre 'KeepHistory' de NzbGet devrait être inférieur à 25000", - "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Le paramètre 'KeepHistory' de NzbGet est défini trop élevé.", + "DownloadClientDownloadStationValidationNoDefaultDestinationDetail": "Vous devez vous connecter à votre poste de travail en tant que {username} et le configurer manuellement dans les paramètres de la DownloadStation sous BT/HTTP/FTP/NZB -> Location.", + "DownloadClientFloodSettingsPostImportTags": "Balises post-importation", + "DownloadClientFloodSettingsRemovalInfo": "{appName} gérera la suppression automatique des torrents sur la base des critères de semences actuels dans Paramètres -> Indexeurs", + "DownloadClientFloodSettingsStartOnAdd": "Démarrer l'ajout", + "DownloadClientNzbVortexMultipleFilesMessage": "Le téléchargement contient plusieurs fichiers et ne se trouve pas dans un dossier de travail : {outputPath}", + "DownloadClientNzbgetValidationKeepHistoryOverMax": "Le paramètre KeepHistory de NzbGet doit être inférieur à 25000", + "DownloadClientNzbgetValidationKeepHistoryOverMaxDetail": "Le paramètre KeepHistory de NzbGet est trop élevé.", "DownloadClientPneumaticSettingsNzbFolder": "Dossier Nzb", "DownloadClientPneumaticSettingsNzbFolderHelpText": "Ce dossier devra être accessible depuis XBMC", "DownloadClientPneumaticSettingsStrmFolder": "Dossier Strm", - "DownloadClientPneumaticSettingsStrmFolderHelpText": "Les fichiers .strm dans ce dossier seront importés par Drone", - "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Télécharger d'abord les premières et les dernières pièces (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsInitialStateHelpText": "État initial pour les torrents ajoutés à qBittorrent. Notez que les torrents forcés ne tiennent pas compte des restrictions de partage", - "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent ne peut pas résoudre le lien magnétique avec la DHT désactivée", + "DownloadClientPneumaticSettingsStrmFolderHelpText": "Les fichiers .strm contenus dans ce dossier seront importés par drone", + "DownloadClientQbittorrentSettingsFirstAndLastFirstHelpText": "Télécharger d'abord le premier et le dernier morceau (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsInitialStateHelpText": "État initial des torrents ajoutés à qBittorrent. Notez que les torrents forcés ne respectent pas les restrictions relatives aux seeds", + "DownloadClientQbittorrentTorrentStateDhtDisabled": "qBittorrent ne peut pas résoudre le lien magnet lorsque le DHT est désactivé", "DownloadClientQbittorrentTorrentStateError": "qBittorrent signale une erreur", - "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} n'a pas réussi à ajouter l'étiquette à qBittorrent.", + "DownloadClientQbittorrentValidationCategoryAddFailureDetail": "{appName} n'a pas pu ajouter l'étiquette à qBittorrent.", "DownloadClientQbittorrentValidationCategoryRecommended": "La catégorie est recommandée", - "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} ne tentera pas d'importer les téléchargements terminés sans catégorie.", + "DownloadClientQbittorrentValidationCategoryRecommendedDetail": "{appName} n'essaiera pas d'importer des téléchargements terminés sans catégorie.", "DownloadClientQbittorrentValidationCategoryUnsupported": "La catégorie n'est pas prise en charge", - "DownloadClientQbittorrentValidationQueueingNotEnabled": "La mise en file d'attente n'est pas activée", - "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "La mise en file d'attente des torrents n'est pas activée dans vos paramètres de qBittorrent. Activez-la dans qBittorrent ou sélectionnez 'Dernier' comme priorité.", - "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent est configuré pour supprimer les torrents lorsqu'ils atteignent leur limite de partage (Share Ratio Limit)", - "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ne pourra pas effectuer le traitement des téléchargements terminés tel que configuré. Vous pouvez résoudre ce problème dans qBittorrent ('Outils -> Options...' dans le menu) en modifiant 'Options -> BitTorrent -> Limitation du ratio de partage' de 'Les supprimer' à 'Les mettre en pause'", - "DownloadClientRTorrentSettingsAddStoppedHelpText": "L'activation ajoutera les torrents et les liens magnétiques à rTorrent dans un état arrêté. Cela peut endommager les fichiers magnétiques.", - "DownloadClientRTorrentSettingsDirectoryHelpText": "Emplacement optionnel pour placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de rTorrent", - "DownloadClientRTorrentSettingsUrlPath": "Chemin d'URL", - "DownloadClientRTorrentSettingsUrlPathHelpText": "Chemin vers le point de terminaison XMLRPC, voir {url}. Il s'agit généralement de RPC2 ou [chemin vers ruTorrent]{url2} lors de l'utilisation de ruTorrent.", - "DownloadClientSabnzbdValidationCheckBeforeDownload": "Désactivez l'option 'Vérifier avant le téléchargement' dans Sabnzbd", - "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "L'utilisation de 'Vérifier avant le téléchargement' affecte la capacité de {appName} à suivre les nouveaux téléchargements. Sabnzbd recommande également 'Abandonner les tâches qui ne peuvent pas être terminées', car c'est plus efficace.", - "DownloadClientSabnzbdValidationDevelopVersion": "Version de développement de Sabnzbd, en supposant la version 3.0.0 ou supérieure.", - "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} pourrait ne pas être en mesure de prendre en charge les nouvelles fonctionnalités ajoutées à SABnzbd lors de l'exécution de versions de développement.", + "DownloadClientQbittorrentValidationQueueingNotEnabled": "Mise en file d'attente non activée", + "DownloadClientQbittorrentValidationQueueingNotEnabledDetail": "La mise en file d'attente des torrents n'est pas activée dans les paramètres de qBittorrent. Activez-la dans qBittorrent ou sélectionnez 'Dernier' comme priorité.", + "DownloadClientQbittorrentValidationRemovesAtRatioLimit": "qBittorrent est configuré pour supprimer les torrents lorsqu'ils atteignent leur limite de ratio de partage", + "DownloadClientQbittorrentValidationRemovesAtRatioLimitDetail": "{appName} ne pourra pas effectuer le traitement des téléchargements terminés tel que configuré. Vous pouvez résoudre ce problème dans qBittorrent ('Outils -> Options...' dans le menu) en remplaçant 'Options -> BitTorrent -> Limitation du ratio de partageg' de 'Les supprimer' par 'Les mettre en pause'", + "DownloadClientRTorrentSettingsAddStoppedHelpText": "L'activation ajoutera des torrents et des magnets à rTorrent dans un état d'arrêt. Cela peut endommager les fichiers magnétiques.", + "DownloadClientRTorrentSettingsDirectoryHelpText": "Emplacement facultatif dans lequel placer les téléchargements. Laisser vide pour utiliser l'emplacement par défaut de rTorrent", + "DownloadClientRTorrentSettingsUrlPath": "Chemin d'url", + "DownloadClientRTorrentSettingsUrlPathHelpText": "Chemin d'accès au point de terminaison XMLRPC, voir {url}. Il s'agit généralement de RPC2 ou de [chemin vers ruTorrent]{url2} lors de l'utilisation de ruTorrent.", + "DownloadClientSabnzbdValidationCheckBeforeDownload": "Désactiver l'option 'Vérifier avant de télécharger' dans Sabnbzd", + "DownloadClientSabnzbdValidationCheckBeforeDownloadDetail": "L'utilisation de 'Vérifier avant de télécharger' affecte la capacité de {appName} à suivre les nouveaux téléchargements. Sabnzbd recommande également 'Abandonner les tâches qui ne peuvent pas être achevées', car c'est plus efficace.", + "DownloadClientSabnzbdValidationDevelopVersion": "Version de développement de Sabnzbd, en supposant une version 3.0.0 ou supérieure.", + "DownloadClientSabnzbdValidationDevelopVersionDetail": "{appName} peut ne pas être en mesure de prendre en charge les nouvelles fonctionnalités ajoutées à SABnzbd lors de l'exécution de versions développées.", "DownloadClientSabnzbdValidationEnableDisableDateSorting": "Désactiver le tri par date", - "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Vous devez désactiver le tri par date pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", + "DownloadClientSabnzbdValidationEnableDisableDateSortingDetail": "Vous devez désactiver le tri par date pour la catégorie {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour résoudre ce problème.", "DownloadClientSabnzbdValidationEnableDisableMovieSorting": "Désactiver le tri des films", - "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Vous devez désactiver le tri des films pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", - "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Désactiver le tri des émissions de télévision", - "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Vous devez désactiver le tri des émissions de télévision pour la catégorie que {appName} utilise afin d'éviter des problèmes lors de l'importation. Rendez-vous dans Sabnzbd pour le résoudre.", - "DownloadClientSabnzbdValidationEnableJobFolders": "Activer les dossiers de tâches", - "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} préfère que chaque téléchargement ait son propre dossier. En ajoutant un astérisque (*) au dossier/chemin, Sabnzbd ne créera pas ces dossiers de tâches. Rendez-vous dans Sabnzbd pour le résoudre.", + "DownloadClientSabnzbdValidationEnableDisableMovieSortingDetail": "Vous devez désactiver le tri des films pour la catégorie utilisée par {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour y remédier.", + "DownloadClientSabnzbdValidationEnableDisableTvSorting": "Désactiver le tri des téléviseurs", + "DownloadClientSabnzbdValidationEnableDisableTvSortingDetail": "Vous devez désactiver le tri TV pour la catégorie {appName} afin d'éviter les problèmes d'importation. Rendez-vous sur le site de Sabnzbd pour y remédier.", + "DownloadClientSabnzbdValidationEnableJobFolders": "Activer les dossiers de travail", + "DownloadClientSabnzbdValidationEnableJobFoldersDetail": "{appName} préfère que chaque téléchargement ait un dossier séparé. Avec * ajouté au dossier/chemin, Sabnzbd ne créera pas ces dossiers de travail. Allez sur Sabnzbd pour résoudre ce problème.", "DownloadClientSabnzbdValidationUnknownVersion": "Version inconnue : {rawVersion}", - "DownloadClientSettingsCategoryHelpText": "Ajouter une catégorie spécifique à {appName} évite les conflits avec des téléchargements non liés à {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée.", - "DownloadClientSettingsPostImportCategoryHelpText": "Catégorie à définir pour {appName} après avoir importé le téléchargement. {appName} ne supprimera pas les torrents de cette catégorie même si le partage est terminé. Laissez vide pour conserver la même catégorie.", - "DownloadClientSettingsDestinationHelpText": "Spécifie manuellement la destination du téléchargement, laissez vide pour utiliser la destination par défaut", - "DownloadClientValidationCategoryMissingDetail": "La catégorie que vous avez entrée n'existe pas dans {clientName}. Créez-la d'abord dans {clientName}.", + "DownloadClientSettingsCategoryHelpText": "L'ajout d'une catégorie spécifique à {appName} permet d'éviter les conflits avec des téléchargements sans rapport avec {appName}. L'utilisation d'une catégorie est facultative, mais fortement recommandée.", + "DownloadClientSettingsPostImportCategoryHelpText": "Catégorie que {appName} doit définir après avoir importé le téléchargement. {appName} ne supprimera pas les torrents de cette catégorie même si l'ensemencement est terminé. Laisser vide pour conserver la même catégorie.", + "DownloadClientSettingsDestinationHelpText": "Spécifie manuellement la destination du téléchargement, laisser vide pour utiliser la destination par défaut", + "DownloadClientValidationCategoryMissingDetail": "La catégorie que vous avez saisie n'existe pas dans {clientName}. Créez-la d'abord dans {clientName}.", "DownloadClientValidationErrorVersion": "La version de {clientName} doit être au moins {requiredVersion}. La version rapportée est {reportedVersion}", - "DownloadClientValidationGroupMissingDetail": "Le groupe que vous avez entré n'existe pas dans {clientName}. Créez-le d'abord dans {clientName}.", + "DownloadClientValidationGroupMissingDetail": "Le groupe que vous avez saisi n'existe pas dans {clientName}. Créez-le d'abord dans {clientName}.", "DownloadClientValidationSslConnectFailure": "Impossible de se connecter via SSL", "DownloadClientValidationUnableToConnect": "Impossible de se connecter à {clientName}", "IndexerIPTorrentsSettingsFeedUrlHelpText": "URL complète du flux RSS généré par IPTorrents, en utilisant uniquement les catégories que vous avez sélectionnées (HD, SD, x264, etc...)", - "IndexerHDBitsSettingsMediums": "Type de médias", + "IndexerHDBitsSettingsMediums": "Supports", "IndexerSettingsAdditionalNewznabParametersHelpText": "Veuillez noter que si vous modifiez la catégorie, vous devrez ajouter des règles requises/restrictives concernant les sous-groupes pour éviter les sorties en langues étrangères.", "IndexerSettingsAllowZeroSize": "Autoriser la taille zéro", "IndexerSettingsAllowZeroSizeHelpText": "L'activation de cette option vous permettra d'utiliser des flux qui ne spécifient pas la taille de la version, mais soyez prudent, les vérifications liées à la taille ne seront pas effectuées.", "IndexerSettingsAnimeCategoriesHelpText": "Liste déroulante, laissez vide pour désactiver les animes", "IndexerSettingsAnimeStandardFormatSearch": "Recherche au format standard pour les animes", "IndexerSettingsAnimeStandardFormatSearchHelpText": "Rechercher également les animes en utilisant la numérotation standard", - "IndexerSettingsApiPathHelpText": "Chemin vers l'API, généralement {url}", + "IndexerSettingsApiPathHelpText": "Chemin d'accès à l'api, généralement {url}", "IndexerSettingsApiUrlHelpText": "Ne le modifiez pas à moins de savoir ce que vous faites, car votre clé API sera envoyée à cet hôte.", "IndexerSettingsCategoriesHelpText": "Liste déroulante, laissez vide pour désactiver les émissions standard/quotidiennes", "IndexerSettingsCookieHelpText": "Si votre site nécessite un cookie de connexion pour accéder au flux RSS, vous devrez le récupérer via un navigateur.", @@ -1641,63 +1640,63 @@ "IndexerValidationJackettAllNotSupportedHelpText": "L'endpoint 'all' de Jackett n'est pas pris en charge, veuillez ajouter les indexeurs individuellement", "IndexerValidationUnableToConnectServerUnavailable": "Impossible de se connecter à l'indexeur, le serveur de l'indexeur est indisponible. Réessayez plus tard. {exceptionMessage}.", "IndexerValidationUnableToConnectTimeout": "Impossible de se connecter à l'indexeur, peut-être en raison d'un délai d'attente. Réessayez ou vérifiez vos paramètres réseau. {exceptionMessage}.", - "NzbgetHistoryItemMessage": "État PAR : {parStatus} - État de décompression : {unpackStatus} - État de déplacement : {moveStatus} - État du script : {scriptStatus} - État de suppression : {deleteStatus} - État de marquage : {markStatus}", - "TorrentBlackholeSaveMagnetFilesHelpText": "Enregistrer le lien magnétique s'il n'y a pas de fichier .torrent disponible (utile uniquement si le client de téléchargement prend en charge les liens magnétiques enregistrés dans un fichier)", - "PostImportCategory": "Catégorie après importation", + "NzbgetHistoryItemMessage": "Statut PAR : {parStatus} - Unpack Status : {unpackStatus} - Move Status : {moveStatus} - Statut du script : {scriptStatus} - Supprimer l'état : {deleteStatus} - Mark Status : {markStatus}", + "TorrentBlackholeSaveMagnetFilesHelpText": "Enregistrer le lien magnétique si aucun fichier .torrent n'est disponible (utile uniquement si le client de téléchargement prend en charge les liens magnétiques enregistrés dans un fichier)", + "PostImportCategory": "Catégorie après l'importation", "BlackholeFolderHelpText": "Dossier dans lequel {appName} stockera le fichier {extension}", - "BlackholeWatchFolder": "Dossier de surveillance", - "BlackholeWatchFolderHelpText": "Dossier à partir duquel {appName} devrait importer les téléchargements terminés", - "DownloadClientDelugeValidationLabelPluginInactive": "Plugin d'étiquetage non activé", - "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Vous devez avoir le plugin d'étiquetage activé dans {clientName} pour utiliser les catégories.", - "DownloadClientDownloadStationSettingsDirectoryHelpText": "Dossier partagé facultatif dans lequel placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de Download Station", - "DownloadClientDownloadStationValidationApiVersion": "Version de l'API de Download Station non prise en charge, elle devrait être au moins {requiredVersion}. Elle prend en charge de {minVersion} à {maxVersion}", - "DownloadClientDownloadStationValidationFolderMissingDetail": "Le dossier '{downloadDir}' n'existe pas, il doit être créé manuellement à l'intérieur du Dossier Partagé '{sharedFolder}'.", - "DownloadClientDownloadStationProviderMessage": "{appName} ne peut pas se connecter à Download Station si l'authentification à deux facteurs est activée sur votre compte DSM", - "DownloadClientFloodSettingsAdditionalTagsHelpText": "Ajoute des propriétés des médias en tant qu'étiquettes. Les indices sont des exemples.", - "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Le paramètre 'KeepHistory' de NzbGet est réglé sur 0, ce qui empêche {appName} de voir les téléchargements terminés.", - "DownloadClientNzbgetValidationKeepHistoryZero": "Le paramètre 'KeepHistory' de NzbGet devrait être supérieur à 0", - "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Premier et dernier prénom", - "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Télécharger dans l'ordre séquentiel (qBittorrent 4.1.0+)", - "DownloadClientQbittorrentSettingsUseSslHelpText": "Utiliser une connexion sécurisée. Consultez Options -> Interface Web -> 'Utiliser HTTPS au lieu de HTTP' dans qBittorrent.", + "BlackholeWatchFolder": "Dossier surveillé", + "BlackholeWatchFolderHelpText": "Dossier à partir duquel {appName} doit importer les téléchargements terminés", + "DownloadClientDelugeValidationLabelPluginInactive": "Plugin d'étiquette non activé", + "DownloadClientDelugeValidationLabelPluginInactiveDetail": "Vous devez avoir activé le plug-in Label dans {clientName} pour utiliser les catégories.", + "DownloadClientDownloadStationSettingsDirectoryHelpText": "Dossier partagé dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement par défaut de Download Station", + "DownloadClientDownloadStationValidationApiVersion": "La version de l'API de la station de téléchargement n'est pas prise en charge, elle doit être au moins {requiredVersion}. Elle est prise en charge de {minVersion} à {maxVersion}", + "DownloadClientDownloadStationValidationFolderMissingDetail": "Le dossier '{downloadDir}' n'existe pas, il doit être créé manuellement dans le dossier partagé '{sharedFolder}'.", + "DownloadClientDownloadStationProviderMessage": "{appName} ne parvient pas à se connecter à Download Station si l'authentification à 2 facteurs est activée sur votre compte DSM", + "DownloadClientFloodSettingsAdditionalTagsHelpText": "Ajoute les propriétés des médias sous forme d'étiquette. Les conseils sont des exemples.", + "DownloadClientNzbgetValidationKeepHistoryZeroDetail": "Le paramètre KeepHistory de NzbGet est fixé à 0, ce qui empêche {appName} de voir les téléchargements terminés.", + "DownloadClientNzbgetValidationKeepHistoryZero": "Le paramètre KeepHistory de NzbGet doit être supérieur à 0", + "DownloadClientQbittorrentSettingsFirstAndLastFirst": "Premier et dernier premiers", + "DownloadClientQbittorrentSettingsSequentialOrderHelpText": "Téléchargement dans l'ordre séquentiel (qBittorrent 4.1.0+)", + "DownloadClientQbittorrentSettingsUseSslHelpText": "Utilisez une connexion sécurisée. Voir Options -> UI Web -> 'Utiliser HTTPS au lieu de HTTP' dans qBittorrent.", "DownloadClientQbittorrentSettingsSequentialOrder": "Ordre séquentiel", - "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent est en train de télécharger les métadonnées", - "DownloadClientQbittorrentTorrentStatePathError": "Impossible d'importer. Le chemin correspond au répertoire de téléchargement de base du client, il est possible que 'Conserver le dossier de niveau supérieur' soit désactivé pour ce torrent ou que 'Disposition du contenu du torrent' ne soit pas définie sur 'Original' ou 'Créer un sous-dossier' ?", + "DownloadClientQbittorrentTorrentStateMetadata": "qBittorrent télécharge des métadonnées", + "DownloadClientQbittorrentTorrentStatePathError": "Impossible d'importer. Le chemin d'accès correspond au répertoire de téléchargement de la base du client, il est possible que l'option 'Conserver le dossier de premier niveau' soit désactivée pour ce torrent ou que l'option 'Disposition du contenu du torrent' ne soit PAS réglée sur 'Original' ou 'Créer un sous-dossier' ?", "DownloadClientQbittorrentTorrentStateStalled": "Le téléchargement est bloqué sans aucune connexion", "DownloadClientQbittorrentTorrentStateUnknown": "État de téléchargement inconnu : {state}", - "DownloadClientRTorrentSettingsAddStopped": "Ajouter Arrêté", - "DownloadClientSettingsAddPaused": "Ajouter en pause", - "DownloadClientSettingsOlderPriority": "Priorité inférieure", + "DownloadClientRTorrentSettingsAddStopped": "Ajout arrêté", + "DownloadClientSettingsAddPaused": "Ajout en pause", + "DownloadClientSettingsOlderPriority": "Priorité plus ancienne", "DownloadClientSettingsRecentPriority": "Priorité récente", "DownloadClientSettingsInitialState": "État initial", "DownloadClientSettingsInitialStateHelpText": "État initial pour les torrents ajoutés à {clientName}", - "DownloadClientSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL de {clientName}, comme {url}", + "DownloadClientSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url {clientName}, tel que {url}", "DownloadClientSettingsUseSslHelpText": "Utiliser une connexion sécurisée lors de la connexion à {clientName}", - "DownloadClientTransmissionSettingsDirectoryHelpText": "Emplacement facultatif pour placer les téléchargements, laissez vide pour utiliser l'emplacement par défaut de Transmission", - "DownloadClientTransmissionSettingsUrlBaseHelpText": "Ajoute un préfixe à l'URL RPC de {clientName}, par exemple {url}, par défaut '{defaultUrl}'", + "DownloadClientTransmissionSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement de transmission par défaut", + "DownloadClientTransmissionSettingsUrlBaseHelpText": "Ajoute un préfixe à l'url rpc de {clientName}, par exemple {url}, la valeur par défaut étant '{defaultUrl}'", "DownloadClientUTorrentTorrentStateError": "uTorrent signale une erreur", "DownloadClientValidationApiKeyIncorrect": "Clé API incorrecte", "DownloadClientValidationApiKeyRequired": "Clé API requise", "DownloadClientValidationAuthenticationFailure": "Échec de l'authentification", - "DownloadClientValidationAuthenticationFailureDetail": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe. Assurez-vous également que l'hôte sur lequel {appName} s'exécute n'est pas bloqué pour l'accès à {clientName} en raison de limitations de liste blanche (WhiteList) dans la configuration de {clientName}.", + "DownloadClientValidationAuthenticationFailureDetail": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe. Vérifiez également que l'hôte qui exécute {appName} n'est pas empêché d'accéder à {clientName} par des limitations de la liste blanche dans la configuration de {clientName}.", "DownloadClientValidationCategoryMissing": "La catégorie n'existe pas", "DownloadClientValidationGroupMissing": "Le groupe n'existe pas", "DownloadClientValidationTestNzbs": "Échec de l'obtention de la liste des NZB : {exceptionMessage}", - "DownloadClientValidationSslConnectFailureDetail": "{appName} ne peut pas se connecter à {clientName} en utilisant SSL. Ce problème pourrait être lié à l'ordinateur. Veuillez essayer de configurer à la fois {appName} et {clientName} pour ne pas utiliser SSL.", + "DownloadClientValidationSslConnectFailureDetail": "{appName} ne parvient pas à se connecter à {clientName} en utilisant SSL. Ce problème peut être lié à l'ordinateur. Veuillez essayer de configurer {appName} et {clientName} pour qu'ils n'utilisent pas SSL.", "DownloadClientValidationTestTorrents": "Échec de l'obtention de la liste des torrents : {exceptionMessage}", "DownloadClientValidationUnknownException": "Exception inconnue : {exception}", - "DownloadClientValidationVerifySsl": "Vérifiez les paramètres SSL", + "DownloadClientValidationVerifySsl": "Vérifier les paramètres SSL", "DownloadClientValidationUnableToConnectDetail": "Veuillez vérifier le nom d'hôte et le port.", - "DownloadClientValidationVerifySslDetail": "Veuillez vérifier votre configuration SSL à la fois sur {clientName} et {appName}", + "DownloadClientValidationVerifySslDetail": "Veuillez vérifier votre configuration SSL sur {clientName} et {appName}", "UnknownDownloadState": "État de téléchargement inconnu : {state}", "DownloadClientSettingsOlderPriorityEpisodeHelpText": "Priorité à utiliser lors de la récupération des épisodes diffusés il y a plus de 14 jours", "DownloadClientSettingsRecentPriorityEpisodeHelpText": "Priorité à utiliser lors de la récupération des épisodes diffusés au cours des 14 derniers jours", "MonitorNewSeasonsHelpText": "Quelles nouvelles saisons doivent être surveillées automatiquement", "MonitorNoNewSeasonsDescription": "Ne pas surveiller automatiquement de nouvelles saisons", - "TorrentBlackholeSaveMagnetFilesExtension": "Enregistrer les extensions de fichiers magnet", - "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension à utiliser pour les liens magnétiques, par défaut '.magnet'", - "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Au lieu de déplacer les fichiers, cela indiquera à {appName} de copier ou de créer un lien (en fonction des paramètres/configuration système)", + "TorrentBlackholeSaveMagnetFilesExtension": "Sauvegarde des fichiers magnétiques Extension", + "TorrentBlackholeSaveMagnetFilesExtensionHelpText": "Extension à utiliser pour les liens magnétiques, la valeur par défaut est '.magnet'", + "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText": "Au lieu de déplacer les fichiers, cela demandera à {appName} de les copier ou de les relier (en fonction des paramètres/de la configuration du système)", "TorrentBlackholeTorrentFolder": "Dossier Torrent", - "XmlRpcPath": "Chemin XML RPC", + "XmlRpcPath": "Chemin d'accès XML RPC", "AddRootFolderError": "Impossible d'ajouter le dossier racine", "NotificationsAppriseSettingsConfigurationKey": "Clé de configuration Apprise", "NotificationsCustomScriptSettingsProviderMessage": "Tester va exécuter le script avec le type d'événement définit sur {eventTypeTest}, assurez-vous que votre script le gère correctement", @@ -1792,7 +1791,7 @@ "NotificationsSettingsUpdateMapPathsFrom": "Mapper les chemins depuis", "NotificationsSettingsUpdateLibrary": "Mettre à jour la bibliothèque", "NotificationsSendGridSettingsApiKeyHelpText": "La clé API générée par SendGrid", - "NotificationsSettingsUpdateMapPathsToHelpText": "Chemin {serviceName}, utilisé pour modifier les chemins des séries quand {serviceName} voit un chemin d'emplacement de bibliothèque différemment de {appName} (nécessite « Mise à jour bibliothèque »)", + "NotificationsSettingsUpdateMapPathsToHelpText": "Chemin {serviceName}, utilisé pour modifier les chemins des séries quand {serviceName} voit un chemin d'emplacement de bibliothèque différemment de {appName} (nécessite 'Mise à jour bibliothèque')", "NotificationsSettingsUpdateMapPathsTo": "Mapper les chemins vers", "NotificationsSignalSettingsUsernameHelpText": "Nom d'utilisateur utilisé pour authentifier les requêtes vers signal-api", "NotificationsSlackSettingsIcon": "Icône", @@ -1872,7 +1871,7 @@ "AutoTaggingSpecificationMaximumYear": "Année maximum", "AutoTaggingSpecificationMinimumYear": "Année minimum", "AutoTaggingSpecificationOriginalLanguage": "Langue", - "AutoTaggingSpecificationQualityProfile": "Profil de Qualité", + "AutoTaggingSpecificationQualityProfile": "Profil de qualité", "AutoTaggingSpecificationRootFolder": "Dossier Racine", "AutoTaggingSpecificationSeriesType": "Type de série", "AutoTaggingSpecificationStatus": "État", @@ -1883,19 +1882,19 @@ "CustomFormatsSpecificationReleaseGroup": "Groupe de versions", "CustomFormatsSpecificationResolution": "Résolution", "CustomFormatsSpecificationSource": "Source", - "ImportListsAniListSettingsAuthenticateWithAniList": "Connection avec AniList", - "ImportListsAniListSettingsImportCancelled": "Importation annulé", - "ImportListsAniListSettingsImportCancelledHelpText": "Media : La série est annulé", - "ImportListsAniListSettingsImportCompleted": "Importation terminé", + "ImportListsAniListSettingsAuthenticateWithAniList": "S'authentifier avec AniList", + "ImportListsAniListSettingsImportCancelled": "Importation annulée", + "ImportListsAniListSettingsImportCancelledHelpText": "Médias : La série est annulée", + "ImportListsAniListSettingsImportCompleted": "Importation terminée", "ImportListsAniListSettingsImportFinished": "Importation terminée", - "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur de la liste à importer", - "ImportListsCustomListSettingsName": "Liste personnalisé", + "ImportListsAniListSettingsUsernameHelpText": "Nom d'utilisateur pour la liste à importer", + "ImportListsCustomListSettingsName": "Liste personnalisée", "ImportListsCustomListValidationAuthenticationFailure": "Échec de l'authentification", - "ImportListsPlexSettingsAuthenticateWithPlex": "Se connecter avec Plex.tv", - "ImportListsPlexSettingsWatchlistName": "Plex Watchlist", + "ImportListsPlexSettingsAuthenticateWithPlex": "S'authentifier avec Plex.tv", + "ImportListsPlexSettingsWatchlistName": "Liste de surveillance Plex", "ImportListsSettingsAccessToken": "Jeton d'accès", "ImportListsSettingsRefreshToken": "Jeton d'actualisation", - "ImportListsSimklSettingsAuthenticatewithSimkl": "Se connecter avec Simkl", + "ImportListsSimklSettingsAuthenticatewithSimkl": "S'authentifier avec Simkl", "ImportListsSonarrSettingsFullUrl": "URL complète", "DownloadClientPriorityHelpText": "Priorité du client de téléchargement de 1 (la plus haute) à 50 (la plus faible). Par défaut : 1. Le Round-Robin est utilisé pour les clients ayant la même priorité.", "CustomFormatsSpecificationRegularExpressionHelpText": "Format personnalisé RegEx est insensible à la casse", @@ -1923,5 +1922,149 @@ "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "Si un torrent est bloqué par le hachage, il peut ne pas être correctement rejeté pendant le RSS/recherche pour certains indexeurs. L'activation de cette fonction permet de le rejeter après que le torrent a été saisi, mais avant qu'il ne soit envoyé au client.", "DownloadClientAriaSettingsDirectoryHelpText": "Emplacement facultatif pour les téléchargements, laisser vide pour utiliser l'emplacement par défaut Aria2", "AddDelayProfileError": "Impossible d'ajouter un nouveau profil de délai, veuillez réessayer.", - "BlocklistReleaseHelpText": "Bloque le téléchargement de cette version par {appName} via RSS ou Recherche automatique" + "BlocklistReleaseHelpText": "Empêche cette version d'être téléchargée par {appName} via RSS ou la recherche automatique", + "NotificationsEmailSettingsUseEncryptionHelpText": "Préférer utiliser le cryptage s'il est configuré sur le serveur, toujours utiliser le cryptage via SSL (port 465 uniquement) ou StartTLS (tout autre port) ou ne jamais utiliser le cryptage", + "LabelIsRequired": "L'étiquette est requise", + "NotificationsEmailSettingsUseEncryption": "Utiliser le cryptage", + "ConnectionSettingsUrlBaseHelpText": "Ajoute un préfixe l'url de {connectionName}, comme {url}", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Destination pour les téléchargements terminés (facultative), laissez ce champ vide pour utiliser le répertoire par défaut de Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Emplacement dans lequel placer les téléchargements (facultatif), laissez vide pour utiliser l'emplacement Deluge par défaut", + "DownloadClientDelugeSettingsDirectory": "Dossier de téléchargement", + "DownloadClientDelugeSettingsDirectoryCompleted": "Dossier de déplacement une fois terminé", + "ClickToChangeIndexerFlags": "Cliquez pour changer les drapeaux de l'indexeur", + "CustomFormatsSpecificationFlag": "Drapeau", + "CustomFilter": "Filtre personnalisé", + "ImportListsTraktSettingsAuthenticateWithTrakt": "S'authentifier avec Trakt", + "SelectIndexerFlags": "Sélectionner les drapeaux de l'indexeur", + "SetIndexerFlags": "Définir les drapeaux de l'indexeur", + "SetIndexerFlagsModalTitle": "{modalTitle} - Définir les drapeaux de l'indexeur", + "KeepAndTagSeries": "Conserver et étiqueter les séries", + "KeepAndUnmonitorSeries": "Série Garder et ne pas surveiller", + "CustomFormatsSpecificationMaximumSizeHelpText": "La version doit être inférieur ou égal à cette taille", + "ImportListsAniListSettingsImportDroppedHelpText": "Liste : Abandonné", + "ImportListsAniListSettingsImportPlanningHelpText": "Liste : Planification à suivre", + "ImportListsAniListSettingsImportPlanning": "Planification des importations", + "ImportListsAniListSettingsImportReleasing": "Importation de la diffusion", + "ImportListsAniListSettingsImportWatching": "Importation de surveillance", + "ImportListsAniListSettingsImportWatchingHelpText": "Liste : En cours de visionnage", + "ImportListsCustomListSettingsUrlHelpText": "L'URL de la liste des séries", + "ImportListsCustomListValidationConnectionError": "Impossible d'envoyer une requête à cette URL. StatusCode : {exceptionStatusCode}", + "ImportListsPlexSettingsWatchlistRSSName": "Liste de surveillance de Plex RSS", + "ImportListsSettingsAuthUser": "Utilisateur Auth", + "ImportListsSettingsExpires": "Expiration", + "ImportListsSimklSettingsListType": "Type de liste", + "ImportListsSimklSettingsName": "Liste de surveillance des utilisateurs de Simkl", + "ImportListsSimklSettingsListTypeHelpText": "Type de liste à partir de laquelle vous cherchez à importer", + "ImportListsSimklSettingsUserListTypeHold": "Tenir", + "ImportListsSimklSettingsUserListTypeDropped": "Abandonné", + "ImportListsSonarrSettingsApiKeyHelpText": "Clé API de l'instance {appName} à importer depuis", + "ImportListsSonarrSettingsRootFoldersHelpText": "Dossiers racine de l'instance source à partir de laquelle l'importation doit être effectuée", + "ImportListsSonarrSettingsTagsHelpText": "Tags de l'instance source à importer", + "ImportListsTraktSettingsAdditionalParameters": "Paramètres supplémentaires", + "ImportListsTraktSettingsAdditionalParametersHelpText": "Paramètres supplémentaires de l'API Trakt", + "ImportListsTraktSettingsLimitHelpText": "Limiter le nombre de séries à obtenir", + "ImportListsTraktSettingsListType": "Type de liste", + "ImportListsTraktSettingsListTypeHelpText": "Type de liste à partir de laquelle vous cherchez à importer", + "ImportListsTraktSettingsPopularListTypeRecommendedMonthShows": "Spectacles recommandés par mois", + "ImportListsTraktSettingsPopularListTypeRecommendedYearShows": "Spectacles recommandés par année", + "ImportListsTraktSettingsPopularListTypeTopWeekShows": "Spectacles les plus regardés par semaine", + "ImportListsTraktSettingsPopularListTypeTopMonthShows": "Spectacles les plus regardés par mois", + "ImportListsTraktSettingsUserListTypeCollection": "Liste des collections d'utilisateurs", + "ImportListsTraktSettingsUserListName": "Utilisateur de Trakt", + "ImportListsTraktSettingsUserListTypeWatch": "Liste de surveillance des utilisateurs", + "ImportListsTraktSettingsUserListUsernameHelpText": "Nom d'utilisateur pour la liste à importer (laisser vide pour utiliser Auth User)", + "ImportListsTraktSettingsUsernameHelpText": "Nom d'utilisateur pour la liste à importer", + "ImportListsTraktSettingsWatchedListFilter": "Filtre de liste surveillée", + "ImportListsTraktSettingsWatchedListTypeAll": "Tous", + "ImportListsTraktSettingsWatchedListTypeCompleted": "100% regardé", + "MetadataSettingsSeasonImages": "Images de la saison", + "MetadataSettingsEpisodeMetadataImageThumbs": "Métadonnées des épisodes Vignettes des images", + "MetadataXmbcSettingsEpisodeMetadataImageThumbsHelpText": "Inclure des balises d'image dans .nfo (nécessite 'Episode Metadata')", + "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo avec les métadonnées complètes de la série", + "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Inclure l'URL de la série TheTVDB dans tvshow.nfo (peut être combiné avec 'Métadonnées de la série')", + "MetadataXmbcSettingsSeriesMetadataEpisodeGuideHelpText": "Inclure l'élément JSON du guide d'épisode dans tvshow.nfo (nécessite 'Métadonnées de la série')", + "MetadataSettingsEpisodeImages": "Images de l'épisode", + "MetadataSettingsEpisodeMetadata": "Métadonnées d'épisode", + "NotificationsSettingsUpdateMapPathsFromHelpText": "Chemin d'accès {appName}, utilisé pour modifier les chemins d'accès aux séries lorsque {serviceName} voit l'emplacement du chemin d'accès à la bibliothèque différemment de {appName} (Nécessite 'Mettre à jour la bibliothèque')", + "ReleaseType": "Type de version", + "ImportListsSimklSettingsUserListTypeWatching": "Regarder", + "ImportListsTraktSettingsLimit": "Limite", + "ImportListsSimklSettingsUserListTypeCompleted": "Terminé", + "ImportListsAniListSettingsImportReleasingHelpText": "Médias : Diffusion actuelle de nouveaux épisodes", + "ImportListsAniListSettingsImportRepeating": "Importation répétée", + "ImportListsAniListSettingsImportRepeatingHelpText": "Liste : En cours de relecture", + "ImportListsSimklSettingsUserListTypePlanToWatch": "Plan de surveillance", + "ImportListsSonarrSettingsFullUrlHelpText": "URL, y compris le port, de l'instance {appName} à importer depuis", + "ImportListsTraktSettingsUserListTypeWatched": "Liste des utilisateurs surveillés", + "Label": "Étiquette", + "MetadataSettingsSeriesMetadataEpisodeGuide": "Guide des épisodes de métadonnées de séries", + "MetadataSettingsSeriesMetadata": "Métadonnées de la série", + "MetadataSettingsSeriesImages": "Série Images", + "CleanLibraryLevel": "Nettoyer le niveau de la bibliothèque", + "EpisodeRequested": "Épisode demandé", + "CustomFormatsSpecificationMinimumSizeHelpText": "La version doit être supérieure à cette taille", + "ImportListsAniListSettingsImportNotYetReleasedHelpText": "Médias : La diffusion n'a pas encore commencé", + "ImportListsAniListSettingsImportPaused": "Importation en pause", + "ImportListsAniListSettingsImportPausedHelpText": "Liste : En attente", + "ImportListsCustomListSettingsUrl": "URL de la liste", + "ImportListStatusAllPossiblePartialFetchHealthCheckMessage": "Toutes les listes requièrent une interaction manuelle en raison de la possibilité de recherches partielles", + "ImportListsAniListSettingsImportCompletedHelpText": "Liste : Surveillance achevée", + "ImportListsAniListSettingsImportDropped": "Importation abandonnée", + "ImportListsAniListSettingsImportFinishedHelpText": "Médias : Tous les épisodes ont été diffusés", + "ImportListsAniListSettingsImportHiatus": "Hiatus d'importation", + "ImportListsAniListSettingsImportHiatusHelpText": "Médias : Série en hiatus", + "ImportListsAniListSettingsImportNotYetReleased": "Importation non encore diffusée", + "ImportListsSimklSettingsShowType": "Type de spectacle", + "ImportListsSimklSettingsShowTypeHelpText": "Type de spectacle que vous souhaitez importer", + "ImportListsSonarrSettingsQualityProfilesHelpText": "Profils de qualité de l'instance source à importer", + "ImportListsSonarrValidationInvalidUrl": "L'URL de {appName} n'est pas valide, vous manque-t-il une base d'URL ?", + "ImportListsSettingsRssUrl": "URL RSS", + "ImportListsImdbSettingsListId": "ID de la liste", + "ImportListsImdbSettingsListIdHelpText": "ID de la liste IMDb (par exemple ls12345678)", + "ImportListsTraktSettingsGenres": "Genres", + "ImportListsTraktSettingsGenresHelpText": "Filtrer les séries par Trakt Genre Slug (séparées par des virgules) Uniquement pour les listes populaires", + "ImportListsTraktSettingsPopularListTypeRecommendedWeekShows": "Spectacles recommandés par semaine", + "ImportListsTraktSettingsPopularListTypeTopAllTimeShows": "Les émissions les plus regardées de tous les temps", + "ImportListsTraktSettingsPopularListTypeTopYearShows": "Spectacles les plus regardés par année", + "ImportListsTraktSettingsPopularListTypeTrendingShows": "Spectacles en vogue", + "ImportListsTraktSettingsPopularName": "Liste populaire de Trakt", + "ImportListsTraktSettingsRating": "Evaluation", + "ImportListsTraktSettingsRatingHelpText": "Série de filtres par plage de valeurs nominales (0-100)", + "ImportListsTraktSettingsWatchedListFilterHelpText": "Si le type de liste est surveillé, sélectionnez le type de série que vous souhaitez importer", + "ImportListsTraktSettingsWatchedListSorting": "Tri de la liste de surveillance", + "ImportListsTraktSettingsWatchedListSortingHelpText": "Si le type de liste est surveillé, sélectionnez l'ordre de tri de la liste", + "ImportListsTraktSettingsListName": "Nom de la liste", + "ImportListsTraktSettingsListNameHelpText": "Nom de la liste à importer, la liste doit être publique ou vous devez avoir accès à la liste", + "ImportListsTraktSettingsPopularListTypeAnticipatedShows": "Spectacles attendus", + "ImportListsTraktSettingsPopularListTypePopularShows": "Spectacles populaires", + "ImportListsTraktSettingsPopularListTypeRecommendedAllTimeShows": "Spectacles recommandés de tous les temps", + "ImportListsTraktSettingsWatchedListTypeInProgress": "En cours", + "ImportListsTraktSettingsYears": "Années", + "ImportListsTraktSettingsYearsHelpText": "Filtrer les séries par année ou par plage d'années", + "ImportListsValidationUnableToConnectException": "Impossible de se connecter à la liste des importations : {exceptionMessage}. Consultez les logs entourant cette erreur pour plus de détails.", + "ImportListsValidationInvalidApiKey": "La clé API n'est pas valide", + "ImportListsValidationTestFailed": "Le test a été interrompu en raison d'une erreur : {exceptionMessage}", + "IndexerFlags": "Drapeaux de l'indexeur", + "ImportListsSonarrSettingsSyncSeasonMonitoring": "Suivi de la saison de synchronisation", + "ImportListsSonarrSettingsSyncSeasonMonitoringHelpText": "La surveillance de la saison de synchronisation à partir de l’instance {appName} si l'option 'Monitor' est activée, elle sera ignorée", + "ListSyncTag": "Balise de synchronisation de liste", + "ListSyncTagHelpText": "Cette étiquette sera ajoutée lorsqu'une série tombera ou ne figurera plus sur votre (vos) liste(s)", + "LogOnly": "Log seulement", + "ListSyncLevelHelpText": "Les séries de la bibliothèque seront traitées en fonction de votre sélection si elles tombent ou ne figurent pas sur votre (vos) liste(s)", + "MetadataPlexSettingsSeriesPlexMatchFile": "Fichier de correspondance Plex série", + "MetadataPlexSettingsSeriesPlexMatchFileHelpText": "Crée un fichier .plexmatch dans le dossier de la série", + "MetadataSettingsSeriesMetadataUrl": "URL des métadonnées de la série", + "NotificationsPlexValidationNoTvLibraryFound": "Au moins une bibliothèque de télévision est requise", + "DatabaseMigration": "Migration des bases de données", + "Filters": "Filtres", + "ReleaseProfileIndexerHelpTextWarning": "L'utilisation d'un indexeur spécifique avec des profils de version peut entraîner la saisie de publications en double.", + "ImportListsMyAnimeListSettingsListStatus": "Statut de la liste", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Type de liste à partir de laquelle vous souhaitez importer, défini sur 'All' pour toutes les listes", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authentifiez-vous avec MyAnimeList", + "CustomFormatsSettingsTriggerInfo": "Un format personnalisé sera appliqué à une version ou à un fichier lorsqu'il correspond à au moins un de chacun des différents types de conditions choisis.", + "ClickToChangeReleaseType": "Cliquez pour changer le type de version", + "EpisodeTitleFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Episode Title:30}`) ou du début (par exemple `{Episode Title:-30}`) sont toutes deux prises en charge. Les titres des épisodes seront automatiquement tronqués en fonction des limitations du système de fichiers si nécessaire.", + "SelectReleaseType": "Sélectionnez le type de version", + "SeriesFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Series Title:30}`) ou du début (par exemple `{Series Title:-30}`) sont toutes deux prises en charge.", + "ReleaseGroupFootNote": "Contrôlez éventuellement la troncature à un nombre maximum d'octets, y compris les points de suspension (`...`). La troncature de la fin (par exemple `{Release Group:30}`) ou du début (par exemple `{Release Group:-30}`) sont toutes deux prises en charge.`)." } diff --git a/src/NzbDrone.Core/Localization/Core/hu.json b/src/NzbDrone.Core/Localization/Core/hu.json index f8ca3b381..f3a6bc1c2 100644 --- a/src/NzbDrone.Core/Localization/Core/hu.json +++ b/src/NzbDrone.Core/Localization/Core/hu.json @@ -17,14 +17,14 @@ "RemoveSelectedItemsQueueMessageText": "Biztosan el akar távolítani {selectedCount} elemet a várólistáról?", "Required": "Kötelező", "Added": "Hozzáadva", - "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {hossz} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", + "ApiKeyValidationHealthCheckMessage": "Kérlek frissítsd az API kulcsot, ami legalább {length} karakter hosszú. Ezt megteheted a Beállításokban, vagy a config file-ban", "ApplyChanges": "Változások alkalmazása", "AppDataLocationHealthCheckMessage": "A frissítés nem lehetséges az alkalmazás adatok törlése nélkül", "AutomaticAdd": "Automatikus hozzáadás", "CountSeasons": "{count} Évad", "DownloadClientCheckNoneAvailableHealthCheckMessage": "Nincs elérhető letöltési kliens", - "DownloadClientRootFolderHealthCheckMessage": "A letöltési kliens {downloadClientName} a letöltéseket a gyökérmappába helyezi. Ne tölts le közvetlenül a gyökérmappába.", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nem lehet kommunikálni a {downloadClientName} -val", + "DownloadClientRootFolderHealthCheckMessage": "A letöltési kliens {downloadClientName} a letöltéseket a gyökérmappába helyezi {rootFolderPath}. Ne tölts le közvetlenül a gyökérmappába.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nem lehet kommunikálni a {downloadClientName}. {errorMessage}", "DownloadClientStatusAllClientHealthCheckMessage": "Az összes letöltési kliens elérhetetlen meghibásodások miatt", "EditSelectedDownloadClients": "Kijelölt letöltési kliensek szerkesztése", "EditSelectedImportLists": "Kijelölt importálási listák szerkesztése", @@ -36,7 +36,7 @@ "Ended": "Vége", "HideAdvanced": "Haladó elrejtése", "ImportListRootFolderMissingRootHealthCheckMessage": "Hiányzó gyökérmappa a/az {rootFolderInfo} importálási listához", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Több gyökérmappa hiányzik a/az {rootFoldersInfo} importálási listához", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Több gyökérmappa hiányzik a/az {rootFolderInfo} importálási listához", "Enabled": "Engedélyezés", "HiddenClickToShow": "Rejtett, kattints a felfedéshez", "ImportListStatusAllUnavailableHealthCheckMessage": "Minden lista elérhetetlen meghibásodások miatt", @@ -94,7 +94,7 @@ "SystemTimeHealthCheckMessage": "A rendszer idő több, mint 1 napot eltér az aktuális időtől. Előfordulhat, hogy az ütemezett feladatok nem futnak megfelelően, amíg az időt nem korrigálják", "Unmonitored": "Nem felügyelt", "UpdateStartupNotWritableHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{startupFolder}' nem írható a(z) '{userName}' felhasználó által.", - "UpdateStartupTranslocationHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{indítási mappa}' az App Translocation mappában található.", + "UpdateStartupTranslocationHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a kezdő mappa '{startupFolder}' az App Translocation mappában található.", "UpdateAvailableHealthCheckMessage": "Új frissítés elérhető", "UpdateUiNotWritableHealthCheckMessage": "A frissítés telepítése nem lehetséges, mert a felhasználó '{userName}' nem rendelkezik írási jogosultsággal a(z) '{uiFolder}' felhasználói felület mappában.", "DownloadClientSortingHealthCheckMessage": "A(z) {downloadClientName} letöltési kliensben engedélyezve van a {sortingMode} rendezés a {appName} kategóriájához. Az import problémák elkerülése érdekében ki kell kapcsolnia a rendezést a letöltési kliensben.", @@ -891,10 +891,9 @@ "DeleteSelectedIndexersMessageText": "Biztosan törölni szeretne {count} kiválasztott indexelőt?", "DownloadClientQbittorrentSettingsContentLayoutHelpText": "Függetlenül attól, hogy a qBittorrent konfigurált tartalomelrendezését használja, az eredeti elrendezést a torrentből, vagy mindig hozzon létre egy almappát (qBittorrent 4.3.2)", "FormatAgeDay": "nap", - "FormatRuntimeMinutes": "{perc} p", + "FormatRuntimeMinutes": "{minutes} p", "DownloadClientRemovesCompletedDownloadsHealthCheckMessage": "A(z) {downloadClientName} letöltési kliens úgy van beállítva, hogy eltávolítsa a befejezett letöltéseket. Ez azt eredményezheti, hogy a letöltések eltávolításra kerülnek az ügyfélprogramból, mielőtt a {appName} importálhatná őket.", "RecyclingBinCleanupHelpTextWarning": "A kiválasztott napoknál régebbi fájlok a lomtárban automatikusan törlődnek", - "ReleaseProfileIndexerHelpTextWarning": "Egy adott indexelő kiadási profilokkal történő használata duplikált kiadások megragadásához vezethet", "RemotePath": "Távoli útvonal", "RelativePath": "Relatív út", "ReleaseProfile": "Release profil", @@ -975,7 +974,6 @@ "DeleteSelectedDownloadClientsMessageText": "Biztosan törölni szeretné a kiválasztott {count} letöltési klienst?", "Tba": "TBA", "SpecialsFolderFormat": "Különleges mappa formátum", - "TablePageSizeMinimum": "A relatív elérési utak a(z) {appName} AppData könyvtárában találhatók", "TorrentDelay": "Torrent Késleltetés", "TorrentBlackhole": "Torrent Blackhole", "TorrentDelayHelpText": "Percek késése, hogy várjon, mielőtt megragad egy torrentet", @@ -1107,7 +1105,7 @@ "DownloadClientFloodSettingsStartOnAdd": "Kezdje a Hozzáadás lehetőséggel", "ImportExtraFiles": "Extra fájlok importálása", "ImportListExclusions": "Listakizárások importálása", - "BlocklistReleaseHelpText": "Letiltja ennek a kiadásnak a letöltését a(z) {app Name} által RSS-en vagy automatikus keresésen keresztül", + "BlocklistReleaseHelpText": "Letiltja ennek a kiadásnak a letöltését a(z) {appName} által RSS-en vagy automatikus keresésen keresztül", "CustomFormatUnknownCondition": "Ismeretlen egyéni formátum feltétele „{implementation}”", "AutoTaggingNegateHelpText": "Ha be van jelölve, az automatikus címkézési szabály nem érvényesül, ha ez a {implementationName} feltétel megfelel.", "CountSeriesSelected": "{count} sorozat kiválasztva", @@ -1380,7 +1378,7 @@ "ProcessingFolders": "Mappák feldolgozása", "ProgressBarProgress": "Haladásjelző sáv: {progress}%", "ProxyType": "Proxy típus", - "RegularExpressionsTutorialLink": "További részletek a reguláris kifejezésekről [itt](https://www.regular-expressions.info/tutorial.html).", + "RegularExpressionsTutorialLink": "További részletek a reguláris kifejezésekről [itt]({url}).", "ReplaceIllegalCharacters": "Cserélje ki az illegális karaktereket", "ResetDefinitionTitlesHelpText": "A definíciócímek és értékek visszaállítása", "ResetDefinitions": "Definíciók visszaállítása", diff --git a/src/NzbDrone.Core/Localization/Core/it.json b/src/NzbDrone.Core/Localization/Core/it.json index d1e93e9d3..8b6adc4bf 100644 --- a/src/NzbDrone.Core/Localization/Core/it.json +++ b/src/NzbDrone.Core/Localization/Core/it.json @@ -110,7 +110,7 @@ "AddAutoTagError": "Impossibile aggiungere un nuovo tag automatico, riprova.", "AddCustomFormat": "Aggiungi Formato Personalizzato", "AddDownloadClient": "Aggiungi Client di Download", - "AddCustomFormatError": "Non riesco ad aggiungere un nuovo formato personalizzato, riprova.", + "AddCustomFormatError": "Impossibile aggiungere un nuovo formato personalizzato, riprova.", "AddDownloadClientError": "Impossibile aggiungere un nuovo client di download, riprova.", "AddDelayProfile": "Aggiungi Profilo di Ritardo", "AddIndexerError": "Impossibile aggiungere un nuovo Indicizzatore, riprova.", @@ -167,7 +167,6 @@ "Custom": "Personalizzato", "CustomFormatJson": "Formato Personalizzato JSON", "Day": "Giorno", - "AddListExclusion": "Aggiungi Lista Esclusioni", "AddedDate": "Aggiunto: {date}", "AirsTbaOn": "Verrà trasmesso su {networkLabel}", "AirsTimeOn": "alle {time} su {networkLabel}", @@ -228,7 +227,7 @@ "DeleteCondition": "Cancella Condizione", "DeleteEpisodeFromDisk": "Cancella episodio dal disco", "DeleteNotification": "Cancella Notifica", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Impossibile comunicare con {downloadClientName}. {errorMessage}", "Connect": "Collegamento", "CustomFormatsSettings": "Formati Personalizzati Impostazioni", "Condition": "Condizione", @@ -249,5 +248,6 @@ "AnimeEpisodeTypeDescription": "Episodi rilasciati utilizzando un numero di episodio assoluto", "AnimeEpisodeTypeFormat": "Numero assoluto dell'episodio ({format})", "AutoRedownloadFailed": "Download fallito", - "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova." + "AddDelayProfileError": "Impossibile aggiungere un nuovo profilo di ritardo, riprova.", + "Cutoff": "Taglio" } diff --git a/src/NzbDrone.Core/Localization/Core/ko.json b/src/NzbDrone.Core/Localization/Core/ko.json index a719741c0..439c144d4 100644 --- a/src/NzbDrone.Core/Localization/Core/ko.json +++ b/src/NzbDrone.Core/Localization/Core/ko.json @@ -9,7 +9,7 @@ "NoHistory": "내역 없음", "SelectAll": "모두 선택", "View": "표시 변경", - "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다.", + "AuthenticationMethodHelpText": "{appName}에 접근하려면 사용자 이름과 암호가 필요합니다", "AddNew": "새로 추가하기", "History": "내역", "Sunday": "일요일" diff --git a/src/NzbDrone.Core/Localization/Core/nb_NO.json b/src/NzbDrone.Core/Localization/Core/nb_NO.json index b763b0c4c..ed9052f14 100644 --- a/src/NzbDrone.Core/Localization/Core/nb_NO.json +++ b/src/NzbDrone.Core/Localization/Core/nb_NO.json @@ -8,5 +8,12 @@ "Absolute": "Absolutt", "Activity": "Aktivitet", "About": "Om", - "CalendarOptions": "Kalenderinnstillinger" + "CalendarOptions": "Kalenderinnstillinger", + "AbsoluteEpisodeNumbers": "Absolutte Episode Numre", + "AddANewPath": "Legg til ny filsti", + "AddConditionImplementation": "Legg til betingelse - {implementationName}", + "AddConditionError": "Ikke mulig å legge til ny betingelse, vennligst prøv igjen", + "AbsoluteEpisodeNumber": "Absolutt Episode Nummer", + "AddAutoTagError": "Ikke mulig å legge til ny automatisk tagg, vennligst prøv igjen", + "Actions": "Handlinger" } diff --git a/src/NzbDrone.Core/Localization/Core/nl.json b/src/NzbDrone.Core/Localization/Core/nl.json index ff993454a..082114cd8 100644 --- a/src/NzbDrone.Core/Localization/Core/nl.json +++ b/src/NzbDrone.Core/Localization/Core/nl.json @@ -38,9 +38,9 @@ "Date": "Datum", "About": "Over", "Actions": "Acties", - "AppDataDirectory": "AppData folder", + "AppDataDirectory": "AppData map", "AptUpdater": "Gebruik apt om de update te installeren", - "BackupNow": "Nu backup nemen", + "BackupNow": "Back-up nu maken", "BeforeUpdate": "Voor Update", "CancelPendingTask": "Ben je zeker dat je deze onafgewerkte taak wil annuleren?", "Clear": "Wis", @@ -170,5 +170,40 @@ "AutoTaggingSpecificationGenre": "Genre(s)", "BackupFolderHelpText": "Relatieve paden zullen t.o.v. de {appName} AppData map bekeken worden", "BindAddress": "Gebonden Adres", - "BindAddressHelpText": "Geldig IP-adres, localhost of '*' voor alle interfaces" + "BindAddressHelpText": "Geldig IP-adres, localhost of '*' voor alle interfaces", + "DelayMinutes": "{delay} minuten", + "FormatAgeMinutes": "minuten", + "EnableInteractiveSearchHelpText": "Zal worden gebruikt wanneer interactief zoeken wordt gebruikt", + "UsenetDelayTime": "Usenet-vertraging: {usenetDelay}", + "MinutesSixty": "60 Minuten: {sixty}", + "RemoveFromDownloadClient": "Verwijder uit download cliënt", + "RemoveSelectedItemsQueueMessageText": "Weet je zeker dat je {selectedCount} items van de wachtrij wilt verwijderen?", + "BlocklistLoadError": "Niet in staat om de blokkeerlijst te laden", + "CustomFormatUnknownConditionOption": "Onbekende optie '{key}' voor conditie '{implementation}'", + "ImportListsSettingsSummary": "Importeer van een andere {appName} of Trakt lijst en regel lijst uitzonderingen", + "TagDetails": "Tagdetails - {label}", + "RemoveSelectedItemQueueMessageText": "Weet je zeker dat je 1 item van de wachtrij wilt verwijderen?", + "EnableAutomaticSearch": "Schakel automatisch zoeken in", + "DeleteTagMessageText": "Weet je zeker dat je de tag '{label}' wil verwijderen?", + "DownloadWarning": "Download waarschuwing: {warningMessage}", + "EnableAutomaticAdd": "Schakel automatisch toevoegen in", + "EnableColorImpairedMode": "Schakel kleurenblindheid-modus in", + "EnableCompletedDownloadHandlingHelpText": "Importeer automatisch voltooide downloads vanuit de download cliënt", + "EnableColorImpairedModeHelpText": "Aangepaste stijl voor gebruikers die kleurenblind zijn om gemakkelijker kleurgecodeerde informatie te onderscheiden", + "EnableInteractiveSearch": "Schakel interactief zoeken in", + "TorrentDelayTime": "Torrent-vertraging: {torrentDelay}", + "WouldYouLikeToRestoreBackup": "Wilt u de back-up {name} herstellen?", + "DeleteNotificationMessageText": "Weet je zeker dat je de notificatie ‘{name}’ wil verwijderen?", + "PrioritySettings": "Prioriteit: {priority}", + "RssSync": "RSS Sync", + "Enable": "Inschakelen", + "EnableAutomaticSearchHelpText": "Zal worden gebruikt wanneer automatische zoekopdrachten worden uitgevoerd via de gebruikersinterface of door {appName}", + "AutomaticUpdatesDisabledDocker": "Automatische updates zijn niet ondersteund wanneer je het docker update mechanisme gebruikt. Je dient de container image up te daten buiten {appName} om of een script te gebruiken", + "ClearBlocklistMessageText": "Weet je zeker dat je de blokkeerlijst wil legen?", + "BlackholeFolderHelpText": "De map waarin {appName} het {extension} bestand opslaat", + "BlackholeWatchFolderHelpText": "De map waaruit {appName} de voltooide downloads dient te importeren", + "Category": "Categorie", + "BlocklistReleaseHelpText": "Voorkom dat deze release opnieuw wordt gedownload door {appName} door een RSS lijst of een automatische zoekopdracht", + "ChangeCategory": "Verander categorie", + "ChownGroup": "chown groep" } diff --git a/src/NzbDrone.Core/Localization/Core/pt.json b/src/NzbDrone.Core/Localization/Core/pt.json index 8f2497675..95931000a 100644 --- a/src/NzbDrone.Core/Localization/Core/pt.json +++ b/src/NzbDrone.Core/Localization/Core/pt.json @@ -129,13 +129,13 @@ "DeleteDownloadClientMessageText": "Tem a certeza que quer eliminar o cliente de transferências \"{name}\"?", "DeleteNotificationMessageText": "Tem a certeza que quer eliminar a notificação \"{name}\"?", "EnableRss": "Activar RSS", - "DeleteSelectedDownloadClientsMessageText": "Tem a certeza de que pretende eliminar o(s) cliente(s) de transferência selecionado(s)?", + "DeleteSelectedDownloadClientsMessageText": "Tem a certeza de que pretende eliminar o(s) cliente(s) de {count} transferência selecionado(s)?", "MaintenanceRelease": "Versão de manutenção: reparações de erros e outras melhorias. Consulte o Histórico de Commits do Github para saber mais", "DeleteBackupMessageText": "Tem a certeza que quer eliminar a cópia de segurança \"{name}\"?", "Exception": "Exceção", "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Pontuação Mínima do Formato Personalizado necessária para contornar o atraso do protocolo preferido", - "ConnectionLostReconnect": "O Radarr tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", - "ConnectionLostToBackend": "O Radarr perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", + "ConnectionLostReconnect": "O {appName} tentará ligar-se automaticamente, ou você pode clicar em Recarregar abaixo.", + "ConnectionLostToBackend": "O {appName} perdeu a ligação com o back-end e precisará ser recarregado para restaurar a funcionalidade.", "CountIndexersSelected": "{count} indexador(es) selecionado(s)", "DeleteImportListMessageText": "Tem a certeza de que pretende eliminar a lista '{name}'?", "DeleteRootFolder": "Eliminar a Pasta Raiz", @@ -143,7 +143,7 @@ "EditSelectedDownloadClients": "Editar Clientes de Transferência Selecionados", "EditSelectedImportLists": "Editar Listas de Importação Selecionadas", "CloneAutoTag": "Clonar Etiqueta Automática", - "DeleteSelectedImportListsMessageText": "Tem a certeza de que pretende eliminar a(s) lista(s) de importação selecionada(s)?", + "DeleteSelectedImportListsMessageText": "Tem a certeza de que pretende eliminar a(s) lista(s) de {count} importação selecionada(s)?", "BypassDelayIfAboveCustomFormatScore": "Ignorar se estiver acima da pontuação de formato personalizado", "CouldNotFindResults": "Nenhum resultado encontrado para \"{term}\"", "CountImportListsSelected": "{count} importar lista(s) selecionada(s)", diff --git a/src/NzbDrone.Core/Localization/Core/pt_BR.json b/src/NzbDrone.Core/Localization/Core/pt_BR.json index 8f5e7e63a..4797f8499 100644 --- a/src/NzbDrone.Core/Localization/Core/pt_BR.json +++ b/src/NzbDrone.Core/Localization/Core/pt_BR.json @@ -9,7 +9,7 @@ "Enabled": "Habilitado", "Ended": "Terminou", "HideAdvanced": "Ocultar opções avançadas", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão faltando nas listas de importação: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Múltiplas pastas raiz estão ausentes nas listas de importação: {rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Todas as listas estão indisponíveis devido a falhas", "ImportMechanismHandlingDisabledHealthCheckMessage": "Ativar gerenciamento de download concluído", "IndexerLongTermStatusUnavailableHealthCheckMessage": "Indexadores indisponíveis devido a falhas por mais de 6 horas: {indexerNames}", @@ -605,11 +605,11 @@ "Importing": "Importando", "IncludeCustomFormatWhenRenaming": "Incluir formato personalizado ao renomear", "IncludeCustomFormatWhenRenamingHelpText": "Incluir no formato de renomeação {Custom Formats}", - "IncludeHealthWarnings": "Incluir Advertências de Saúde", + "IncludeHealthWarnings": "Incluir Alertas de Saúde", "IndexerDownloadClientHelpText": "Especifique qual cliente de download é usado para baixar deste indexador", "IndexerOptionsLoadError": "Não foi possível carregar as opções do indexador", "IndexerPriority": "Prioridade do indexador", - "IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado como desempate para lançamentos iguais ao obter lançamentos, o {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS", + "IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado ao capturar lançamentos como desempate para lançamentos iguais, {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS", "IndexerSettings": "Configurações do indexador", "IndexersLoadError": "Não foi possível carregar os indexadores", "IndexersSettingsSummary": "Indexadores e opções de indexador", @@ -772,7 +772,6 @@ "RegularExpressionsTutorialLink": "Mais detalhes sobre expressões regulares podem ser encontrados [aqui]({url}).", "ReleaseProfile": "Perfil de Lançamento", "ReleaseProfileIndexerHelpText": "Especifique a qual indexador o perfil se aplica", - "ReleaseProfileIndexerHelpTextWarning": "Usar um indexador específico com perfis de lançamento pode levar à captura de lançamentos duplicados", "ReleaseProfileTagSeriesHelpText": "Os perfis de lançamento serão aplicados a séries com pelo menos uma tag correspondente. Deixe em branco para aplicar a todas as séries", "ReleaseProfiles": "Perfis de Lançamentos", "ReleaseProfilesLoadError": "Não foi possível carregar perfis de lançamentos", @@ -990,7 +989,7 @@ "AgeWhenGrabbed": "Tempo de vida (quando obtido)", "DelayingDownloadUntil": "Atrasando o download até {date} às {time}", "DeletedReasonEpisodeMissingFromDisk": "O {appName} não conseguiu encontrar o arquivo no disco, então o arquivo foi desvinculado do episódio no banco de dados", - "DeletedReasonManual": "O arquivo foi excluído por meio da IU", + "DeletedReasonManual": "O arquivo foi excluído usando {appName} manualmente ou por outra ferramenta por meio da API", "DownloadFailed": "Download Falhou", "DestinationRelativePath": "Caminho Relativo de Destino", "DownloadIgnoredEpisodeTooltip": "Download do Episódio Ignorado", @@ -1327,7 +1326,7 @@ "MoveSeriesFoldersToRootFolder": "Gostaria de mover as pastas da série para '{destinationRootFolder}'?", "PreviewRename": "Prévia da Renomeação", "PreviewRenameSeason": "Prévia da Renomeação para esta temporada", - "PreviousAiringDate": "Exibição Anterior: {data}", + "PreviousAiringDate": "Exibição Anterior: {date}", "SeasonInformation": "Informações da Temporada", "SeasonDetails": "Detalhes da Temporada", "SelectAll": "Selecionar Tudo", @@ -1615,7 +1614,7 @@ "DownloadClientValidationTestTorrents": "Falha ao obter a lista de torrents: {exceptionMessage}", "DownloadClientValidationUnableToConnect": "Não foi possível conectar-se a {clientName}", "DownloadClientValidationUnableToConnectDetail": "Verifique o nome do host e a porta.", - "DownloadClientValidationUnknownException": "Exceção desconhecida: {exceção}", + "DownloadClientValidationUnknownException": "Exceção desconhecida: {exception}", "DownloadClientValidationVerifySsl": "Verifique as configurações de SSL", "DownloadClientValidationVerifySslDetail": "Verifique sua configuração SSL em {clientName} e {appName}", "DownloadClientVuzeValidationErrorVersion": "Versão do protocolo não suportada, use Vuze 5.0.0.0 ou superior com o plugin Vuze Web Remote.", @@ -1756,7 +1755,7 @@ "NotificationsKodiSettingsCleanLibraryHelpText": "Limpar biblioteca após atualização", "NotificationsKodiSettingsDisplayTime": "Tempo de Exibição", "NotificationsKodiSettingsGuiNotification": "Notificação GUI", - "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca ao Importar & Renomear?", + "NotificationsKodiSettingsUpdateLibraryHelpText": "Atualizar biblioteca em Importar e Renomear?", "NotificationsMailgunSettingsApiKeyHelpText": "A chave API gerada pelo MailGun", "NotificationsMailgunSettingsSenderDomain": "Domínio do Remetente", "NotificationsMailgunSettingsUseEuEndpoint": "Usar EU Endpoint", @@ -2050,5 +2049,24 @@ "CustomFilter": "Filtro Personalizado", "Filters": "Filtros", "Label": "Rótulo", - "LabelIsRequired": "Rótulo é requerido" + "LabelIsRequired": "Rótulo é requerido", + "ConnectionSettingsUrlBaseHelpText": "Adiciona um prefixo a URL {connectionName}, como {url}", + "ReleaseType": "Tipo de Lançamento", + "DownloadClientDelugeSettingsDirectory": "Diretório de Download", + "DownloadClientDelugeSettingsDirectoryCompleted": "Mover para o Diretório Quando Concluído", + "DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Local opcional para mover os downloads concluídos, deixe em branco para usar o local padrão do Deluge", + "DownloadClientDelugeSettingsDirectoryHelpText": "Local opcional para colocar downloads, deixe em branco para usar o local padrão do Deluge", + "EpisodeRequested": "Episódio Pedido", + "ReleaseProfileIndexerHelpTextWarning": "Definir um indexador específico em um perfil de lançamento fará com que esse perfil seja aplicado apenas a lançamentos desse indexador.", + "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Autenticar com MyAnimeList", + "ImportListsMyAnimeListSettingsListStatus": "Status da Lista", + "ImportListsMyAnimeListSettingsListStatusHelpText": "Tipo de lista da qual você deseja importar, defina como 'Todas' para todas as listas", + "CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos.", + "EpisodeTitleFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Episode Title:30}`) ou do início (por exemplo, `{Episode Title:-30}`) é suportado. Os títulos dos episódios serão automaticamente truncados de acordo com as limitações do sistema de arquivos, se necessário.", + "NotificationsTelegramSettingsIncludeAppNameHelpText": "Opcionalmente, prefixe o título da mensagem com {appName} para diferenciar notificações de diferentes aplicativos", + "ReleaseGroupFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Release Group:30}`) ou do início (por exemplo, `{Release Group:-30}`) é suportado.`).", + "ClickToChangeReleaseType": "Clique para alterar o tipo de lançamento", + "NotificationsTelegramSettingsIncludeAppName": "Incluir {appName} no Título", + "SelectReleaseType": "Selecionar o Tipo de Lançamento", + "SeriesFootNote": "Opcionalmente, controle o truncamento para um número máximo de bytes, incluindo reticências (`...`). Truncar do final (por exemplo, `{Series Title:30}`) ou do início (por exemplo, `{Series Title:-30}`) é suportado." } diff --git a/src/NzbDrone.Core/Localization/Core/ro.json b/src/NzbDrone.Core/Localization/Core/ro.json index 68252092b..183427ba3 100644 --- a/src/NzbDrone.Core/Localization/Core/ro.json +++ b/src/NzbDrone.Core/Localization/Core/ro.json @@ -20,7 +20,7 @@ "ApplyTagsHelpTextAdd": "Adăugare: adăugați etichetele la lista de etichete existentă", "ApplyTagsHelpTextReplace": "Înlocuire: înlocuiți etichetele cu etichetele introduse (nu introduceți etichete pentru a șterge toate etichetele)", "CancelPendingTask": "Sigur doriți să anulați această sarcină în așteptare?", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}.", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "Nu pot comunica cu {downloadClientName}. {errorMessage}", "CloneCustomFormat": "Clonați format personalizat", "Close": "Închide", "Delete": "Șterge", @@ -132,7 +132,6 @@ "AuthForm": "Formulare (Pagina de autentificare)", "AuthenticationMethodHelpText": "Solicitați nume utilizator și parola pentru a accesa {appName}", "AuthenticationRequired": "Autentificare necesara", - "Authentication": "", "AddNewSeriesError": "Nu s-au putut încărca rezultatele căutării, încercați din nou.", "AddSeriesWithTitle": "Adăugați {title}", "AlreadyInYourLibrary": "Deja în biblioteca dvs.", @@ -199,5 +198,7 @@ "AppUpdated": "{appName} actualizat", "ShowRelativeDatesHelpText": "Afișați datele relative (Azi / Ieri / etc) sau absolute", "WeekColumnHeader": "Antetul coloanei săptămânii", - "TimeFormat": "Format ora" + "TimeFormat": "Format ora", + "CustomFilter": "Filtru personalizat", + "CustomFilters": "Filtre personalizate" } diff --git a/src/NzbDrone.Core/Localization/Core/ru.json b/src/NzbDrone.Core/Localization/Core/ru.json index f5271efdd..cf5197d5c 100644 --- a/src/NzbDrone.Core/Localization/Core/ru.json +++ b/src/NzbDrone.Core/Localization/Core/ru.json @@ -20,7 +20,7 @@ "HiddenClickToShow": "Скрыто, нажмите чтобы показать", "HideAdvanced": "Скрыть расширенные", "ImportListRootFolderMissingRootHealthCheckMessage": "Отсутствует корневая папка для импортирования списка(ов): {rootFolderInfo}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "Для импортируемых списков отсутствуют несколько корневых папок: {rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "Все листы недоступны из-за ошибок", "ImportListStatusUnavailableHealthCheckMessage": "Листы недоступны из-за ошибок: {importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleHealthCheckMessage": "Включить обработку завершенной загрузки, если это возможно", @@ -197,7 +197,6 @@ "AlreadyInYourLibrary": "Уже в вашей библиотеке", "Always": "Всегда", "Conditions": "Условия", - "AddAutoTag": "", "AbsoluteEpisodeNumber": "Абсолютный номер эпизода", "CustomFormatsSettings": "Настройки пользовательских форматов", "Daily": "Ежедневно", @@ -211,5 +210,11 @@ "CustomFormatsSpecificationRegularExpression": "Регулярное выражение", "CustomFormatsSpecificationReleaseGroup": "Релиз группа", "CustomFormatsSpecificationResolution": "Разрешение", - "CustomFormatsSpecificationSource": "Источник" + "CustomFormatsSpecificationSource": "Источник", + "AddAutoTag": "Добавить автоматический тег", + "AddAutoTagError": "Не удалось добавить новый авто тег, пожалуйста повторите попытку.", + "AddListExclusionError": "Не удалось добавить новое исключение из списка. Повторите попытку.", + "AddImportListExclusionError": "Не удалось добавить новое исключение из списка импорта. Повторите попытку.", + "AddListExclusion": "Добавить исключение из списка", + "AddDelayProfileError": "Не удалось добавить новый профиль задержки. Повторите попытку." } diff --git a/src/NzbDrone.Core/Localization/Core/sk.json b/src/NzbDrone.Core/Localization/Core/sk.json index 0967ef424..3bb1bc1e3 100644 --- a/src/NzbDrone.Core/Localization/Core/sk.json +++ b/src/NzbDrone.Core/Localization/Core/sk.json @@ -1 +1,41 @@ -{} +{ + "Activity": "Aktivita", + "Absolute": "Celkom", + "AddImportList": "Pridať zoznam importov", + "AddConditionImplementation": "Pridať podmienku - {implementationName}", + "AddConnectionImplementation": "Pridať pripojenie - {implementationName}", + "AddImportListExclusion": "Pridať vylúčenie zoznamu importov", + "AddDownloadClientImplementation": "Pridať klienta pre sťahovanie - {implementationName}", + "AddImportListImplementation": "Pridať zoznam importov - {implementationName}", + "AddReleaseProfile": "Pridať profil vydania", + "AddRemotePathMapping": "Pridať vzdialené mapovanie ciest", + "AddToDownloadQueue": "Pridať do fronty sťahovania", + "AllFiles": "Všetky súbory", + "AddAutoTag": "Pridať automatickú značku", + "AddCondition": "Pridať podmienku", + "AddingTag": "Pridávanie značky", + "Add": "Pridať", + "AgeWhenGrabbed": "Vek (po uchopení)", + "All": "Všetko", + "Age": "Vek", + "About": "O", + "Actions": "Akcie", + "AddAutoTagError": "Nie je možné pridať novú automatickú značku, skúste to znova.", + "AddConnection": "Pridať podmienku", + "AddConditionError": "Nie je možné pridať novú podmienku, skúste to znova.", + "AfterManualRefresh": "Po ručnom obnovení", + "AllResultsAreHiddenByTheAppliedFilter": "Použitý filter skryje všetky výsledky", + "Always": "Vždy", + "AnalyticsEnabledHelpText": "Odosielajte anonymné informácie o používaní a chybách na servery aplikácie {appName}. Zahŕňa to informácie o vašom prehliadači, ktoré stránky webového používateľského rozhrania {appName} používate, hlásenia chýb, ako aj verziu operačného systému a spustenia. Tieto informácie použijeme na stanovenie priorít funkcií a opráv chýb.", + "RestartRequiredHelpTextWarning": "Vyžaduje sa reštart, aby sa zmeny prejavili", + "ApplyTagsHelpTextAdd": "Pridať: Pridať značky do existujúceho zoznamu značiek", + "AddRootFolder": "Pridať koreňový priečinok", + "AddedToDownloadQueue": "Pridané do fronty sťahovania", + "Analytics": "Analytika", + "AddIndexerImplementation": "Pridať Indexer - {implementationName}", + "AddQualityProfile": "Pridať profil kvality", + "Added": "Pridané", + "AlreadyInYourLibrary": "Už vo vašej knižnici", + "AlternateTitles": "Alternatívny názov", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Ako použiť značky na vybratých klientov na sťahovanie" +} diff --git a/src/NzbDrone.Core/Localization/Core/tr.json b/src/NzbDrone.Core/Localization/Core/tr.json index c1633f7ce..3a45b393b 100644 --- a/src/NzbDrone.Core/Localization/Core/tr.json +++ b/src/NzbDrone.Core/Localization/Core/tr.json @@ -6,7 +6,7 @@ "AddConnection": "Bağlantı Ekle", "AddConditionImplementation": "Koşul Ekle - {implementationName}", "EditConnectionImplementation": "Koşul Ekle - {implementationName}", - "AddConnectionImplementation": "Koşul Ekle - {implementationName}", + "AddConnectionImplementation": "Bağlantı Ekle - {implementationName}", "AddIndexerImplementation": "Yeni Dizin Ekle - {implementationName}", "EditIndexerImplementation": "Koşul Ekle - {implementationName}", "AddToDownloadQueue": "İndirme kuyruğuna ekleyin", @@ -16,7 +16,7 @@ "Actions": "Eylemler", "AbsoluteEpisodeNumber": "Mutlak Bölüm Numarası", "AddListExclusionError": "Yeni bir hariç tutma listesi eklenemiyor, lütfen tekrar deneyin.", - "AddListExclusion": "Liste Hariç Tutma Ekle", + "AddListExclusion": "Hariç Tutma Listesine Ekle", "AddNewRestriction": "Yeni kısıtlama ekle", "AddedDate": "Eklendi: {date}", "Activity": "Etkinlik", @@ -65,5 +65,83 @@ "AddListError": "Yeni bir liste eklenemiyor, lütfen tekrar deneyin.", "AddNew": "Yeni Ekle", "AddListExclusionSeriesHelpText": "Dizilerin {appName} listeler tarafından eklenmesini önleyin", - "AddRootFolderError": "Kök klasör eklenemiyor" + "AddRootFolderError": "Kök klasör eklenemiyor", + "CountImportListsSelected": "{count} içe aktarma listesi seçildi", + "CustomFormatsSpecificationFlag": "Bayrak", + "ClickToChangeIndexerFlags": "Dizin oluşturucu bayraklarını değiştirmek için tıklayın", + "ClickToChangeReleaseGroup": "Sürüm grubunu değiştirmek için tıklayın", + "AppUpdated": "{appName} Güncellendi", + "ApplicationURL": "Uygulama URL'si", + "ApplyTagsHelpTextAdd": "Ekle: Etiketleri mevcut etiket listesine ekleyin", + "ApplyTagsHelpTextHowToApplyIndexers": "Seçilen indeksleyicilere etiketler nasıl uygulanır?", + "ApplyTagsHelpTextRemove": "Kaldır: Girilen etiketleri kaldırın", + "AuthenticationRequiredPasswordHelpTextWarning": "Yeni şifre girin", + "AuthenticationRequiredUsernameHelpTextWarning": "Yeni kullanıcı adınızı girin", + "AuthenticationMethodHelpTextWarning": "Lütfen geçerli bir kimlik doğrulama yöntemi seçin", + "AutoTaggingRequiredHelpText": "Otomatik etiketleme kuralının uygulanabilmesi için bu {implementationName} koşulunun eşleşmesi gerekir. Aksi takdirde tek bir {implementationName} eşleşmesi yeterlidir.", + "BlocklistLoadError": "Engellenenler listesi yüklenemiyor", + "BypassDelayIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip sürüm olduğunda gecikmeyi atlayın", + "ConnectionLostToBackend": "{appName}'ın arka uçla bağlantısı kesildi ve işlevselliğin geri kazanılması için yeniden yüklenmesi gerekecek.", + "CustomFormatJson": "Özel JSON Formatı", + "AutomaticAdd": "Otomatik Ekle", + "CustomFilter": "Özel Filtre", + "CustomFormatUnknownConditionOption": "'{implementation}' koşulu için bilinmeyen seçenek '{key}'", + "AutoTagging": "Otomatik Etiketleme", + "AutoTaggingNegateHelpText": "İşaretlenirse, {implementationName} koşulu eşleştiğinde otomatik etiketleme kuralı uygulanmayacaktır.", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Seçilen indirme istemcilerine etiketler nasıl uygulanır?", + "ApplyTagsHelpTextHowToApplyImportLists": "Seçilen içe aktarma listelerine etiketler nasıl uygulanır?", + "AuthenticationRequiredHelpText": "İstekler için Kimlik doğrulamanın gereklilik ayarını değiştirin. Riskleri anlamadığınız sürece değiştirmeyin.", + "AutoTaggingLoadError": "Otomatik etiketleme yüklenemiyor", + "BypassDelayIfAboveCustomFormatScore": "Özel Format Koşullarının Üstündeyse Baypas Et", + "Clone": "Klon", + "CouldNotFindResults": "'{term}' için herhangi bir sonuç bulunamadı", + "AudioLanguages": "Ses Dilleri", + "ApplicationUrlHelpText": "Bu uygulamanın http(s)://, bağlantı noktası ve URL tabanını içeren harici URL'si", + "ApplyChanges": "Değişiklikleri Uygula", + "BlocklistAndSearch": "Engellenenler Listesi ve Arama", + "BlocklistAndSearchHint": "Engellenenler listesine ekledikten sonra yenisini aramaya başlayın", + "BlocklistAndSearchMultipleHint": "Engellenenler listesine ekledikten sonra yedekleri aramaya başlayın", + "BlocklistMultipleOnlyHint": "Yedekleri aramadan engelleme listesi", + "BlocklistOnly": "Yalnızca Engellenenler Listesi", + "BlocklistOnlyHint": "Yenisini aramadan engelleme listesi", + "BlocklistReleaseHelpText": "Bu sürümün {appName} tarafından RSS veya Otomatik Arama yoluyla yeniden indirilmesi engelleniyor", + "BypassDelayIfAboveCustomFormatScoreHelpText": "Sürümün puanı, yapılandırılan minimum özel format puanından yüksek olduğunda bypass'ı etkinleştirin", + "BypassDelayIfAboveCustomFormatScoreMinimumScoreHelpText": "Tercih edilen protokolde gecikmeyi atlamak için gereken Minimum Özel Format Puanı", + "BypassDelayIfHighestQuality": "En Yüksek Kalitedeyse Atla", + "ChangeCategory": "Kategoriyi Değiştir", + "ChangeCategoryHint": "İndirme İstemcisi'nden indirme işlemini 'İçe Aktarma Sonrası Kategorisi' olarak değiştirir", + "ChangeCategoryMultipleHint": "İndirme istemcisinden indirmeleri 'İçe Aktarma Sonrası Kategorisi' olarak değiştirir", + "ChooseImportMode": "İçe Aktarma Modunu Seçin", + "ClearBlocklist": "Engellenenler listesini temizle", + "ConnectSettingsSummary": "Bildirimler, medya sunucularına/oynatıcılara bağlantılar ve özel komut dosyaları", + "CountDownloadClientsSelected": "{count} indirme istemcisi seçildi", + "CustomFormatUnknownCondition": "Bilinmeyen Özel Biçim koşulu '{implementation}'", + "CustomFormatsSpecificationRegularExpression": "Düzenli ifade", + "AppDataDirectory": "Uygulama Veri Dizini", + "ChownGroup": "Chown Grubu", + "ConditionUsingRegularExpressions": "Bu koşul Normal İfadeler kullanılarak eşleşir. `\\^$.|?*+()[{` karakterlerinin özel anlamlara sahip olduğunu ve `\\` ile kaçılması gerektiğini unutmayın", + "BlackholeFolderHelpText": "{appName} uygulamasının {extension} dosyasını depolayacağı klasör", + "BlackholeWatchFolder": "İzleme Klasörü", + "BypassDelayIfAboveCustomFormatScoreMinimumScore": "Minimum Özel Format Puanı", + "Cancel": "Vazgeç", + "Category": "Kategori", + "CertificateValidationHelpText": "HTTPS sertifika doğrulamasının sıkılığını değiştirin. Riskleri anlamadığınız sürece değişmeyin.", + "CloneCondition": "Klon Durumu", + "CountIndexersSelected": "{count} dizin oluşturucu seçildi", + "CustomFormatsSpecificationRegularExpressionHelpText": "Özel Format RegEx Büyük/Küçük Harfe Duyarsızdır", + "AutoRedownloadFailed": "Yeniden İndirme Başarısız", + "AutoRedownloadFailedFromInteractiveSearch": "Etkileşimli Aramadan Yeniden İndirme Başarısız Oldu", + "AutoRedownloadFailedFromInteractiveSearchHelpText": "Başarısız indirmeler, etkileşimli aramada bulunduğunda otomatik olarak farklı bir versiyonu arayın ve indirmeyi deneyin", + "ApplyTagsHelpTextReplace": "Değiştir: Etiketleri girilen etiketlerle değiştirin (tüm etiketleri kaldırmak için etiket girmeyin)", + "AuthenticationMethod": "Kimlik Doğrulama Yöntemi", + "AuthenticationRequired": "Kimlik Doğrulama Gerekli", + "AuthenticationRequiredWarning": "Kimlik doğrulaması olmadan uzaktan erişimi engellemek için, {appName}'da artık kimlik doğrulamanın etkinleştirilmesini gerektiriyor. İsteğe bağlı olarak yerel adresler için kimlik doğrulamayı devre dışı bırakabilirsiniz.", + "ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz", + "ClearBlocklistMessageText": "Engellenenler listesindeki tüm öğeleri temizlemek istediğinizden emin misiniz?", + "AutomaticUpdatesDisabledDocker": "Docker güncelleme mekanizması kullanıldığında otomatik güncellemeler doğrudan desteklenmez. Kapsayıcı görüntüsünü {appName} dışında güncellemeniz veya bir komut dosyası kullanmanız gerekecek", + "ConnectionLostReconnect": "{appName} otomatik bağlanmayı deneyecek veya aşağıda yeniden yükle seçeneğini işaretleyebilirsiniz.", + "BlackholeWatchFolderHelpText": "{appName} uygulamasının tamamlanmış indirmeleri içe aktaracağı klasör", + "AuthenticationRequiredPasswordConfirmationHelpTextWarning": "Yeni şifreyi onayla", + "BindAddressHelpText": "Tüm arayüzler için geçerli IP adresi, localhost veya '*'", + "CloneAutoTag": "Otomatik Etiketi Klonla" } diff --git a/src/NzbDrone.Core/Localization/Core/uk.json b/src/NzbDrone.Core/Localization/Core/uk.json index 0f30d9911..a514c1313 100644 --- a/src/NzbDrone.Core/Localization/Core/uk.json +++ b/src/NzbDrone.Core/Localization/Core/uk.json @@ -38,5 +38,71 @@ "AddList": "Додати список", "AddListError": "Неможливо додати новий список, спробуйте ще раз.", "AddListExclusionError": "Неможливо додати новий виняток зі списку, спробуйте ще раз.", - "AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків" + "AddListExclusionSeriesHelpText": "Заборонити додавання серіалів до {appName} зі списків", + "AllSeriesAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром", + "AlternateTitles": "Альтернативна назва", + "Analytics": "Аналітика", + "Apply": "Застосувати", + "ApplyTags": "Застосувати теги", + "ApplyTagsHelpTextAdd": "Додати: додати теги до наявного списку тегів", + "ApplyTagsHelpTextHowToApplyImportLists": "Як застосувати теги до вибраних списків імпорту", + "ApplyTagsHelpTextRemove": "Видалити: видалити введені теги", + "ApplyTagsHelpTextHowToApplyIndexers": "Як застосувати теги до вибраних індексаторів", + "ApplyTagsHelpTextReplace": "Замінити: Змінити наявні теги на введені теги (залишіть порожнім, щоб очистити всі теги)", + "AuthenticationMethodHelpTextWarning": "Виберіть дійсний метод автентифікації", + "AirsDateAtTimeOn": "{date} о {time} на {networkLabel}", + "AirDate": "Дата трансляції", + "AddRemotePathMapping": "Додати віддалений шлях", + "AddRemotePathMappingError": "Не вдалося додати нове зіставлення віддаленого шляху, спробуйте ще раз.", + "AnalyticsEnabledHelpText": "Надсилайте анонімну інформацію про використання та помилки на сервери {appName}. Це включає інформацію про ваш веб-переглядач, які сторінки {appName} WebUI ви використовуєте, звіти про помилки, а також версію ОС і часу виконання. Ми будемо використовувати цю інформацію, щоб визначити пріоритети функцій і виправлення помилок.", + "ApiKeyValidationHealthCheckMessage": "Будь ласка оновіть ключ API, щоб він містив принаймні {length} символів. Ви можете зробити це в налаштуваннях або в файлі конфігурації", + "AppDataLocationHealthCheckMessage": "Оновлення буде неможливим, щоб запобігти видаленню AppData під час оновлення", + "ApplyTagsHelpTextHowToApplyDownloadClients": "Як застосувати теги до вибраних клієнтів завантаження", + "AllResultsAreHiddenByTheAppliedFilter": "Всі результати приховані фільтром", + "AudioInfo": "Аудіо інформація", + "Age": "Вік", + "All": "Всі", + "Anime": "Аніме", + "AgeWhenGrabbed": "Вік (коли схоплено)", + "AnimeEpisodeFormat": "Формат серії аніме", + "ApiKey": "API Ключ", + "ApplicationURL": "URL програми", + "AppDataDirectory": "Каталог AppData", + "AptUpdater": "Використовуйте apt для інсталяції оновлення", + "AddRootFolder": "Додати корневий каталог", + "AllTitles": "Усі Назви", + "Always": "Завжди", + "AddNewSeriesError": "Не вдалося завантажити результати пошуку, спробуйте ще.", + "AlreadyInYourLibrary": "Вже у вашій бібліотеці", + "AddDelayProfileError": "Неможливо додати новий профіль затримки, будь ласка спробуйте ще.", + "AddNewSeriesHelpText": "Додати новий серіал легко, просто почніть вводити назву серіалу, який ви хочете додати.", + "AddNewSeriesRootFolderHelpText": "Підпапка '{folder}' буде створена автоматично", + "AddNewSeriesSearchForMissingEpisodes": "Почніть пошук відсутніх епізодів", + "AddNotificationError": "Не вдалося додати нове сповіщення, спробуйте ще раз.", + "AddQualityProfile": "Додати профіль якості", + "AddQualityProfileError": "Не вдалося додати новий профіль якості, спробуйте ще раз.", + "AddReleaseProfile": "Додати профіль релізу", + "AirsTimeOn": "{time} на {networkLabel}", + "AllFiles": "Всі файли", + "AirsTomorrowOn": "Завтра о {time} на {networkLabel}", + "AnalyseVideoFiles": "Аналізувати відео файли", + "AnalyseVideoFilesHelpText": "Отримайте з файлів інформацію про відео, таку як роздільна здатність, час виконання та кодек. Це вимагає, щоб {appName} читав частини файлу, що може спричинити високу дискову або мережеву активність під час сканування.", + "Any": "Будь-який", + "AppUpdated": "{appName} Оновлено", + "ApplicationUrlHelpText": "Зовнішня URL-адреса цієї програми, включаючи http(s)://, порт і базу URL-адрес", + "ApplyChanges": "Застосувати зміни", + "AudioLanguages": "Мови аудіо", + "AuthForm": "Форми (сторінка входу)", + "Authentication": "Автентифікація", + "AuthenticationMethod": "Метод автентифікації", + "Yes": "Так", + "AuthenticationRequired": "Потрібна Автентифікація", + "UpdateAll": "Оновити все", + "WhatsNew": "Що нового ?", + "Yesterday": "Вчора", + "AddedToDownloadQueue": "Додано в чергу на завантаження", + "AuthenticationRequiredWarning": "Щоб запобігти віддаленому доступу без автентифікації, {appName} тепер вимагає ввімкнення автентифікації. За бажанням можна вимкнути автентифікацію з локальних адрес.", + "AutomaticUpdatesDisabledDocker": "Автоматичні оновлення не підтримуються безпосередньо під час використання механізму оновлення Docker. Вам потрібно буде оновити зображення контейнера за межами {appName} або скористатися сценарієм", + "AuthenticationRequiredPasswordHelpTextWarning": "Введіть новий пароль", + "AuthenticationRequiredUsernameHelpTextWarning": "Введіть нове ім'я користувача" } diff --git a/src/NzbDrone.Core/Localization/Core/zh_CN.json b/src/NzbDrone.Core/Localization/Core/zh_CN.json index f95f32f6b..d04a2d4b3 100644 --- a/src/NzbDrone.Core/Localization/Core/zh_CN.json +++ b/src/NzbDrone.Core/Localization/Core/zh_CN.json @@ -62,7 +62,7 @@ "Metadata": "元数据", "CountSeasons": "第 {count} 季", "DownloadClientCheckNoneAvailableHealthCheckMessage": "无可用的下载客户端", - "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{downloadClientName}进行通讯", + "DownloadClientCheckUnableToCommunicateWithHealthCheckMessage": "无法与{downloadClientName}进行通讯. {errorMessage}", "DownloadClientRootFolderHealthCheckMessage": "下载客户端{downloadClientName}将下载内容放在根文件夹{rootFolderPath}中。您不应该下载到根文件夹。", "DownloadClientSortingHealthCheckMessage": "下载客户端{downloadClientName}已为{appName}的分类启用{sortingMode}排序。您应该在下载客户端中禁用排序,以避免导入问题。", "DownloadClientStatusAllClientHealthCheckMessage": "所有下载客户端都不可用", @@ -71,7 +71,7 @@ "Enabled": "已启用", "Ended": "已完结", "ImportListRootFolderMissingRootHealthCheckMessage": "在导入列表中缺少根目录文件夹:{rootFolderInfo}", - "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{rootFoldersInfo}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "导入列表中缺失多个根目录文件夹:{rootFolderInfo}", "ImportListStatusAllUnavailableHealthCheckMessage": "所有的列表因错误不可用", "ImportListStatusUnavailableHealthCheckMessage": "列表因错误不可用:{importListNames}", "ImportMechanismEnableCompletedDownloadHandlingIfPossibleMultiComputerHealthCheckMessage": "如果可能,启用完整的下载处理(不支持多台计算机)", @@ -276,7 +276,7 @@ "RemotePathMappingGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}将文件下载在{path}中,但{appName}无法找到此目录。您可能需要调整文件夹的权限。", "RemotePathMappingLocalWrongOSPathHealthCheckMessage": "本地下载客户端{downloadClientName}将文件下载在{path}中,但这不是有效的{osName}路径。查看您的下载客户端设置。", "RemotePathMappingRemoteDownloadClientHealthCheckMessage": "远程下载客户端{downloadClientName}报告了{path}中的文件,但此目录似乎不存在。可能缺少远程路径映射。", - "IRCLinkText": "#Libera上的{appName}", + "IRCLinkText": "#sonarr - Libera", "LiberaWebchat": "Libera聊天", "RemotePathMappingFilesBadDockerPathHealthCheckMessage": "您正在使用Docker;下载客户端{downloadClientName}报告了{path}中的文件,但这不是有效的{osName}中的路径。查看Docker路径映射并更新下载客户端设置。", "RemotePathMappingFilesGenericPermissionsHealthCheckMessage": "下载客户端{downloadClientName}报告的文件在{path},但{appName}无法查看此目录。您可能需要调整文件夹的权限。", @@ -588,7 +588,6 @@ "DownloadIgnored": "忽略下载", "DownloadIgnoredEpisodeTooltip": "集下载被忽略", "EditAutoTag": "编辑自动标签", - "AddAutoTagError": "无法添加新的自动标签,请重试。", "AddImportListExclusionError": "无法添加新排除列表,请再试一次。", "AddIndexer": "添加索引器", "AddImportList": "添加导入列表", @@ -825,7 +824,7 @@ "SelectEpisodes": "选择剧集", "SelectSeason": "选择季", "LibraryImportTipsQualityInEpisodeFilename": "确保您的文件在其文件名中包含质量。例如:`episode.s02e15.bluray.mkv`", - "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExamp}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", + "LibraryImportTipsSeriesUseRootFolder": "将{appName}指向包含所有电视节目的文件夹,而不是特定的一个。例如“`{goodFolderExample}`”而不是“`{badFolderExample}`”。此外,每个剧集都必须有单独的文件夹位于根/库文件夹下。", "ListQualityProfileHelpText": "质量配置列表项将添加", "SeriesIndexFooterMissingMonitored": "缺失集(剧集已监控)", "SeriesIsMonitored": "剧集被监控", @@ -907,7 +906,7 @@ "TorrentDelay": "Torrent延时", "ToggleUnmonitoredToMonitored": "未监控,单击进行监控", "Upcoming": "即将播出", - "ProgressBarProgress": "进度栏位于{Progress}%", + "ProgressBarProgress": "进度栏位于{progress}%", "Usenet": "Usenet", "Week": "周", "Standard": "标准", @@ -966,7 +965,7 @@ "MappedNetworkDrivesWindowsService": "映射网络驱动器在作为Windows服务运行时不可用,请参阅[常见问题解答]({url})获取更多信息。", "Mapping": "映射", "MaximumLimits": "最大限制", - "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", + "MarkAsFailedConfirmation": "是否确实要将“{sourceTitle}”标记为失败?", "Max": "最大的", "MaximumSingleEpisodeAgeHelpText": "在整季搜索期间,当该季的最后一集比此设置旧时,只允许获取整季包。仅限标准剧集。填写 0 可禁用此设置。", "MaximumSize": "最大文件体积", @@ -994,7 +993,7 @@ "MoreDetails": "更多详细信息", "More": "更多", "MoveAutomatically": "自动移动", - "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{originalPath}' ?", + "MoveSeriesFoldersToNewPath": "是否将剧集文件从 '{originalPath}' 移动到 '{destinationPath}' ?", "MoveSeriesFoldersMoveFiles": "是,移动文件", "MultiEpisode": "多集", "MultiEpisodeStyle": "多集风格", @@ -1082,7 +1081,6 @@ "RefreshAndScan": "刷新并扫描", "RefreshAndScanTooltip": "刷新信息并扫描磁盘", "ReleaseProfileIndexerHelpText": "指定配置文件应用于哪个索引器", - "ReleaseProfileIndexerHelpTextWarning": "使用有发布配置的特定索引器可能会导致重复获取发布", "ReleaseRejected": "发布被拒绝", "ReleaseSceneIndicatorAssumingTvdb": "推测TVDB编号。", "ReleaseSceneIndicatorMappedNotRequested": "在此搜索中未包含已映射的剧集。", @@ -1585,7 +1583,7 @@ "IndexerSettingsSeedRatioHelpText": "种子在停止之前应达到的比率,留空使用下载客户端的默认值。 比率应至少为 1.0 并遵循索引器规则", "IndexerValidationTestAbortedDueToError": "测试因错误而中止:{exceptionMessage}", "IndexerValidationSearchParametersNotSupported": "索引器不支持所需的搜索参数", - "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {异常消息}。", + "IndexerValidationUnableToConnectHttpError": "无法连接到索引器,请检查您的 DNS 设置并确保 IPv6 正在运行或已禁用。 {exceptionMessage}。", "IndexerValidationUnableToConnectInvalidCredentials": "无法连接到索引器,凭据无效。{exceptionMessage}。", "IndexerValidationUnableToConnectResolutionFailure": "与索引器连接失败。 请检查与索引器服务器和 DNS 的连接。{exceptionMessage}。", "IndexerValidationUnableToConnectServerUnavailable": "无法连接到索引器,索引器的服务器不可用。 请稍后再试。{exceptionMessage}。", @@ -1601,7 +1599,7 @@ "DownloadClientFloodSettingsPostImportTagsHelpText": "导入下载后附加标签。", "DownloadClientFloodSettingsStartOnAdd": "添加并开始", "DownloadClientFloodSettingsTagsHelpText": "下载的初始标签。 要被识别,下载必须具有所有初始标签。 这可以避免与不相关的下载发生冲突。", - "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因:{错误描述}", + "DownloadClientFreeboxAuthenticationError": "Freebox API 身份验证失败。 原因 {errorDescription}", "DownloadClientFreeboxSettingsApiUrl": "API 地址", "DownloadClientFreeboxSettingsAppToken": "App Token", "DownloadClientFreeboxUnableToReachFreebox": "无法访问 Freebox API。请检查“主机名”、“端口”或“使用 SSL”的设置(错误: {exceptionMessage})", @@ -1762,7 +1760,6 @@ "NotificationsNtfySettingsServerUrl": "服务器 URL", "NotificationsNtfySettingsPasswordHelpText": "密码,可选", "NotificationsPushBulletSettingSenderIdHelpText": "发送通知的设备 ID,使用 pushbullet.com 设备 URL 中的 device_iden 参数值,或者留空来自行发送", - "NotificationsPushBulletSettingsAccessToken": "", "NotificationsPushBulletSettingsChannelTags": "频道标签", "NotificationsPushBulletSettingsChannelTagsHelpText": "通知的目标频道标签列表", "NotificationsPushBulletSettingsDeviceIds": "设备 ID", @@ -1788,7 +1785,7 @@ "DownloadClientAriaSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置", "DownloadClientPriorityHelpText": "下载客户端优先级,从1(最高)到50(最低),默认为1。具有相同优先级的客户端将轮换使用。", "IndexerSettingsRejectBlocklistedTorrentHashes": "抓取时舍弃列入黑名单的种子散列值", - "ChangeCategory": "改变分类", + "ChangeCategory": "修改分类", "IgnoreDownload": "忽略下载", "IgnoreDownloads": "忽略下载", "IgnoreDownloadsHint": "阻止 {appName} 进一步处理这些下载", @@ -1810,5 +1807,6 @@ "ChangeCategoryHint": "将下载从下载客户端更改为“导入后类别”", "IgnoreDownloadHint": "阻止 {appName} 进一步处理此下载", "IndexerSettingsRejectBlocklistedTorrentHashesHelpText": "如果 torrent 的哈希被屏蔽了,某些索引器在使用RSS或者搜索期间可能无法正确拒绝它,启用此功能将允许在抓取 torrent 之后但在将其发送到客户端之前拒绝它。", - "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。" + "RemoveQueueItemRemovalMethodHelpTextWarning": "“从下载客户端移除”将从下载客户端移除下载内容和文件。", + "AutoTaggingSpecificationOriginalLanguage": "语言" } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 999d4d067..f7d878f16 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -174,10 +174,16 @@ namespace NzbDrone.Core.MediaFiles fileInfoStopwatch.Stop(); _logger.Trace("Reprocessing existing files complete for: {0} [{1}]", series, decisionsStopwatch.Elapsed); - var filesOnDisk = GetNonVideoFiles(series.Path); - var possibleExtraFiles = FilterPaths(series.Path, filesOnDisk); - RemoveEmptySeriesFolder(series.Path); + + var possibleExtraFiles = new List(); + + if (_diskProvider.FolderExists(series.Path)) + { + var extraFiles = GetNonVideoFiles(series.Path); + possibleExtraFiles = FilterPaths(series.Path, extraFiles); + } + CompletedScanning(series, possibleExtraFiles); } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 8dee12c2b..cd810a457 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.MediaFiles public string OriginalFilePath { get; set; } public string SceneName { get; set; } public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } public QualityModel Quality { get; set; } public IndexerFlags IndexerFlags { get; set; } public MediaInfoModel MediaInfo { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 1f2f3cb9c..2518df234 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.MediaFiles public EpisodeFile MoveEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) { - var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path)); + var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path), null, localEpisode.CustomFormats); EnsureEpisodeFolder(episodeFile, localEpisode, filePath); @@ -89,7 +89,7 @@ namespace NzbDrone.Core.MediaFiles public EpisodeFile CopyEpisodeFile(EpisodeFile episodeFile, LocalEpisode localEpisode) { - var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path)); + var filePath = _buildFileNames.BuildFilePath(localEpisode.Episodes, localEpisode.Series, episodeFile, Path.GetExtension(localEpisode.Path), null, localEpisode.CustomFormats); EnsureEpisodeFolder(episodeFile, localEpisode, filePath); @@ -123,6 +123,11 @@ namespace NzbDrone.Core.MediaFiles episodeFile.RelativePath = series.Path.GetRelativePath(destinationFilePath); + if (localEpisode is not null) + { + localEpisode.FileNameBeforeRename = episodeFile.RelativePath; + } + if (localEpisode is not null && _scriptImportDecider.TryImport(episodeFilePath, destinationFilePath, localEpisode, episodeFile, mode) is var scriptImportDecision && scriptImportDecision != ScriptImportDecision.DeferMove) { if (scriptImportDecision == ScriptImportDecision.RenameRequested) @@ -130,7 +135,6 @@ namespace NzbDrone.Core.MediaFiles try { MoveEpisodeFile(episodeFile, series, episodeFile.Episodes); - localEpisode.FileRenamedAfterScriptImport = true; } catch (SameFilenameException) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs new file mode 100644 index 000000000..a2012de14 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateReleaseHash.cs @@ -0,0 +1,41 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators +{ + public class AggregateReleaseHash : IAggregateLocalEpisode + { + public int Order => 1; + + public LocalEpisode Aggregate(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + var releaseHash = GetReleaseHash(localEpisode.FileEpisodeInfo); + + if (releaseHash.IsNullOrWhiteSpace()) + { + releaseHash = GetReleaseHash(localEpisode.DownloadClientEpisodeInfo); + } + + if (releaseHash.IsNullOrWhiteSpace()) + { + releaseHash = GetReleaseHash(localEpisode.FolderEpisodeInfo); + } + + localEpisode.ReleaseHash = releaseHash; + + return localEpisode; + } + + private string GetReleaseHash(ParsedEpisodeInfo episodeInfo) + { + // ReleaseHash doesn't make sense for a FullSeason, since hashes should be specific to a file + if (episodeInfo == null || episodeInfo.FullSeason) + { + return null; + } + + return episodeInfo.ReleaseHash; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs index 5beabf7d5..53418a6ff 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Aggregation/Aggregators/AggregateSubtitleInfo.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Parser; @@ -30,18 +31,23 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation.Aggregators return localEpisode; } + if (localEpisode.Episodes.Empty()) + { + return localEpisode; + } + var firstEpisode = localEpisode.Episodes.First(); var episodeFile = firstEpisode.EpisodeFile.Value; - localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path); + localEpisode.SubtitleInfo = CleanSubtitleTitleInfo(episodeFile, path, localEpisode.FileNameBeforeRename); return localEpisode; } - public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path) + public SubtitleTitleInfo CleanSubtitleTitleInfo(EpisodeFile episodeFile, string path, string fileNameBeforeRename) { var subtitleTitleInfo = LanguageParser.ParseSubtitleLanguageInformation(path); - var episodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.RelativePath); + var episodeFileTitle = Path.GetFileNameWithoutExtension(fileNameBeforeRename ?? episodeFile.RelativePath); var originalEpisodeFileTitle = Path.GetFileNameWithoutExtension(episodeFile.OriginalFilePath) ?? string.Empty; if (subtitleTitleInfo.TitleFirst && (episodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase) || originalEpisodeFileTitle.Contains(subtitleTitleInfo.RawTitle, StringComparison.OrdinalIgnoreCase))) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 74e2a71e6..39c3c849f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -95,6 +95,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport episodeFile.SeasonNumber = localEpisode.SeasonNumber; episodeFile.Episodes = localEpisode.Episodes; episodeFile.ReleaseGroup = localEpisode.ReleaseGroup; + episodeFile.ReleaseHash = localEpisode.ReleaseHash; episodeFile.Languages = localEpisode.Languages; // Prefer the release type from the download client, folder and finally the file so we have the most accurate information. @@ -122,6 +123,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport else { episodeFile.IndexerFlags = localEpisode.IndexerFlags; + episodeFile.ReleaseType = localEpisode.ReleaseType; } // Fall back to parsed information if history is unavailable or missing @@ -175,9 +177,9 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport { if (localEpisode.ScriptImported) { - _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles); + _existingExtraFiles.ImportExtraFiles(localEpisode.Series, localEpisode.PossibleExtraFiles, localEpisode.FileNameBeforeRename); - if (localEpisode.FileRenamedAfterScriptImport) + if (localEpisode.FileNameBeforeRename != episodeFile.RelativePath) { _extraService.MoveFilesAfterRename(localEpisode.Series, episodeFile); } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs index 58af3323d..f1fcd03cf 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Manual/ManualImportService.cs @@ -25,7 +25,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual { List GetMediaFiles(int seriesId, int? seasonNumber); List GetMediaFiles(string path, string downloadId, int? seriesId, bool filterExistingFiles); - ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, List languages, int indexerFlags); + ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, List languages, int indexerFlags, ReleaseType releaseType); } public class ManualImportService : IExecute, IManualImportService @@ -139,7 +139,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual return ProcessFolder(path, path, downloadId, seriesId, filterExistingFiles); } - public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, List languages, int indexerFlags) + public ManualImportItem ReprocessItem(string path, string downloadId, int seriesId, int? seasonNumber, List episodeIds, string releaseGroup, QualityModel quality, List languages, int indexerFlags, ReleaseType releaseType) { var rootFolder = Path.GetDirectoryName(path); var series = _seriesService.GetSeries(seriesId); @@ -169,9 +169,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual localEpisode.ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup; localEpisode.Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? languageParse : languages; localEpisode.Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality; + localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; + localEpisode.ReleaseType = releaseType; + localEpisode.CustomFormats = _formatCalculator.ParseCustomFormat(localEpisode); localEpisode.CustomFormatScore = localEpisode.Series?.QualityProfile?.Value.CalculateCustomFormatScore(localEpisode.CustomFormats) ?? 0; - localEpisode.IndexerFlags = (IndexerFlags)indexerFlags; return MapItem(_importDecisionMaker.GetDecision(localEpisode, downloadClientItem), rootFolder, downloadId, null); } @@ -199,7 +201,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Manual ReleaseGroup = releaseGroup.IsNullOrWhiteSpace() ? Parser.Parser.ParseReleaseGroup(path) : releaseGroup, Languages = languages?.Count <= 1 && (languages?.SingleOrDefault() ?? Language.Unknown) == Language.Unknown ? LanguageParser.ParseLanguages(path) : languages, Quality = quality.Quality == Quality.Unknown ? QualityParser.ParseQuality(path) : quality, - IndexerFlags = (IndexerFlags)indexerFlags + IndexerFlags = (IndexerFlags)indexerFlags, + ReleaseType = releaseType }; return MapItem(new ImportDecision(localEpisode, new Rejection("Episodes not selected")), rootFolder, downloadId, null); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs index 64734ed01..4d13eda6f 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/FullSeasonSpecification.cs @@ -23,8 +23,8 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications if (localEpisode.FileEpisodeInfo.FullSeason) { - _logger.Debug("Single episode file detected as containing all episodes in the season"); - return Decision.Reject("Single episode file contains all episodes in seasons"); + _logger.Debug("Single episode file detected as containing all episodes in the season due to no episode parsed from the file name."); + return Decision.Reject("Single episode file contains all episodes in seasons. Review file name or manually import"); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index 064f15157..bd8d66025 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -129,28 +129,30 @@ namespace NzbDrone.Core.MediaFiles [EventHandleOrder(EventHandleOrder.Last)] public void Handle(EpisodeFileDeletedEvent message) { - if (_configService.DeleteEmptyFolders) + if (!_configService.DeleteEmptyFolders || message.Reason == DeleteMediaFileReason.MissingFromDisk) { - var series = message.EpisodeFile.Series.Value; - var seriesPath = series.Path; - var folder = message.EpisodeFile.Path.GetParentPath(); + return; + } - while (seriesPath.IsParentPath(folder)) - { - if (_diskProvider.FolderExists(folder)) - { - _diskProvider.RemoveEmptySubfolders(folder); - } + var series = message.EpisodeFile.Series.Value; + var seriesPath = series.Path; + var folder = message.EpisodeFile.Path.GetParentPath(); - folder = folder.GetParentPath(); + while (seriesPath.IsParentPath(folder)) + { + if (_diskProvider.FolderExists(folder)) + { + _diskProvider.RemoveEmptySubfolders(folder); } - _diskProvider.RemoveEmptySubfolders(seriesPath); + folder = folder.GetParentPath(); + } - if (_diskProvider.FolderEmpty(seriesPath)) - { - _diskProvider.DeleteFolder(seriesPath, true); - } + _diskProvider.RemoveEmptySubfolders(seriesPath); + + if (_diskProvider.FolderEmpty(seriesPath)) + { + _diskProvider.DeleteFolder(seriesPath, true); } } } diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 023b52d85..5aa4cf514 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -23,7 +23,7 @@ namespace NzbDrone.Core.Messaging.Commands } public virtual bool UpdateScheduledTask => true; - public virtual string CompletionMessage => "Completed"; + public virtual string CompletionMessage => null; public virtual bool RequiresDiskAccess => false; public virtual bool IsExclusive => false; public virtual bool IsLongRunning => false; diff --git a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs index f8aef8654..c5d89bbaf 100644 --- a/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs +++ b/src/NzbDrone.Core/MetadataSource/ISearchForNewSeries.cs @@ -9,5 +9,6 @@ namespace NzbDrone.Core.MetadataSource List SearchForNewSeriesByImdbId(string imdbId); List SearchForNewSeriesByAniListId(int aniListId); List SearchForNewSeriesByTmdbId(int tmdbId); + List SearchForNewSeriesByMyAnimeListId(int malId); } } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 76efea07d..9313b0661 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -90,6 +90,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return results; } + public List SearchForNewSeriesByMyAnimeListId(int malId) + { + var results = SearchForNewSeries($"mal:{malId}"); + + return results; + } + public List SearchForNewSeriesByTmdbId(int tmdbId) { var results = SearchForNewSeries($"tmdb:{tmdbId}"); diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 1dbd1fe9d..649f69581 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -233,15 +233,33 @@ namespace NzbDrone.Core.Notifications public void Handle(ManualInteractionRequiredEvent message) { + var series = message.Episode.Series; + var mess = ""; + + if (series != null) + { + mess = GetMessage(series, message.Episode.Episodes, message.Episode.ParsedEpisodeInfo.Quality); + } + + if (mess.IsNullOrWhiteSpace() && message.TrackedDownload.DownloadItem != null) + { + mess = message.TrackedDownload.DownloadItem.Title; + } + + if (mess.IsNullOrWhiteSpace()) + { + return; + } + var manualInteractionMessage = new ManualInteractionRequiredMessage { - Message = GetMessage(message.Episode.Series, message.Episode.Episodes, message.Episode.ParsedEpisodeInfo.Quality), - Series = message.Episode.Series, + Message = mess, + Series = series, Quality = message.Episode.ParsedEpisodeInfo.Quality, Episode = message.Episode, TrackedDownload = message.TrackedDownload, - DownloadClientInfo = message.TrackedDownload.DownloadItem.DownloadClientInfo, - DownloadId = message.TrackedDownload.DownloadItem.DownloadId, + DownloadClientInfo = message.TrackedDownload.DownloadItem?.DownloadClientInfo, + DownloadId = message.TrackedDownload.DownloadItem?.DownloadId, Release = message.Release }; diff --git a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs index 4198ce38c..d35810db8 100644 --- a/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs +++ b/src/NzbDrone.Core/Notifications/Ntfy/NtfyProxy.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; - using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; @@ -115,18 +114,18 @@ namespace NzbDrone.Core.Notifications.Ntfy { try { - requestBuilder.Headers.Add("X-Title", title); - requestBuilder.Headers.Add("X-Message", message); - requestBuilder.Headers.Add("X-Priority", settings.Priority.ToString()); + requestBuilder.AddQueryParam("title", title); + requestBuilder.AddQueryParam("message", message); + requestBuilder.AddQueryParam("priority", settings.Priority.ToString()); if (settings.Tags.Any()) { - requestBuilder.Headers.Add("X-Tags", settings.Tags.Join(",")); + requestBuilder.AddQueryParam("tags", settings.Tags.Join(",")); } if (!settings.ClickUrl.IsNullOrWhiteSpace()) { - requestBuilder.Headers.Add("X-Click", settings.ClickUrl); + requestBuilder.AddQueryParam("click", settings.ClickUrl); } if (!settings.AccessToken.IsNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs index ecc2c4392..85e24ce99 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexTv/PlexTvService.cs @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv var clientIdentifier = _configService.PlexClientIdentifier; - var requestBuilder = new HttpRequestBuilder("https://metadata.provider.plex.tv/library/sections/watchlist/all") + var requestBuilder = new HttpRequestBuilder("https://discover.provider.plex.tv/library/sections/watchlist/all") .Accept(HttpAccept.Json) .AddQueryParam("clientID", clientIdentifier) .AddQueryParam("context[device][product]", BuildInfo.AppName) @@ -107,7 +107,8 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv .AddQueryParam("context[device][platformVersion]", "7") .AddQueryParam("context[device][version]", BuildInfo.Version.ToString()) .AddQueryParam("includeFields", "title,type,year,ratingKey") - .AddQueryParam("includeElements", "Guid") + .AddQueryParam("excludeElements", "Image") + .AddQueryParam("includeGuids", "1") .AddQueryParam("sort", "watchlistedAt:desc") .AddQueryParam("type", (int)PlexMediaType.Show) .AddQueryParam("X-Plex-Container-Size", pageSize) diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index 3d056d115..1edbfa909 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -18,47 +18,65 @@ namespace NzbDrone.Core.Notifications.Telegram public override void OnGrab(GrabMessage grabMessage) { - _proxy.SendNotification(EPISODE_GRABBED_TITLE, grabMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_GRABBED_TITLE_BRANDED : EPISODE_GRABBED_TITLE; + + _proxy.SendNotification(title, grabMessage.Message, Settings); } public override void OnDownload(DownloadMessage message) { - _proxy.SendNotification(EPISODE_DOWNLOADED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_DOWNLOADED_TITLE_BRANDED : EPISODE_DOWNLOADED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { - _proxy.SendNotification(EPISODE_DELETED_TITLE, deleteMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? EPISODE_DELETED_TITLE_BRANDED : EPISODE_DELETED_TITLE; + + _proxy.SendNotification(title, deleteMessage.Message, Settings); } public override void OnSeriesAdd(SeriesAddMessage message) { - _proxy.SendNotification(SERIES_ADDED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? SERIES_ADDED_TITLE_BRANDED : SERIES_ADDED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { - _proxy.SendNotification(SERIES_DELETED_TITLE, deleteMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? SERIES_DELETED_TITLE_BRANDED : SERIES_DELETED_TITLE; + + _proxy.SendNotification(title, deleteMessage.Message, Settings); } public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck) { - _proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE; + + _proxy.SendNotification(title, healthCheck.Message, Settings); } public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck) { - _proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings); + var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE; + + _proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings); } public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage) { - _proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE; + + _proxy.SendNotification(title, updateMessage.Message, Settings); } public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message) { - _proxy.SendNotification(MANUAL_INTERACTION_REQUIRED_TITLE, message.Message, Settings); + var title = Settings.IncludeAppNameInTitle ? MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED : MANUAL_INTERACTION_REQUIRED_TITLE; + + _proxy.SendNotification(title, message.Message, Settings); } public override ValidationResult Test() diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs index fbeb625df..f1cc39f1a 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs @@ -54,10 +54,11 @@ namespace NzbDrone.Core.Notifications.Telegram { try { + const string brandedTitle = "Sonarr - Test Notification"; const string title = "Test Notification"; const string body = "This is a test message from Sonarr"; - SendNotification(title, body, settings); + SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings); } catch (Exception ex) { diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index ede7b3ad3..2b768ce45 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -32,6 +32,9 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(3, Label = "NotificationsTelegramSettingsSendSilently", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsSendSilentlyHelpText")] public bool SendSilently { get; set; } + [FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")] + public bool IncludeAppNameInTitle { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs index a5a1dec6d..d114a88db 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookBase.cs @@ -178,6 +178,8 @@ namespace NzbDrone.Core.Notifications.Webhook DownloadClient = message.DownloadClientInfo?.Name, DownloadClientType = message.DownloadClientInfo?.Type, DownloadId = message.DownloadId, + DownloadStatus = message.TrackedDownload.Status.ToString(), + DownloadStatusMessages = message.TrackedDownload.StatusMessages.Select(x => new WebhookDownloadStatusMessage(x)).ToList(), CustomFormatInfo = new WebhookCustomFormatInfo(remoteEpisode.CustomFormats, remoteEpisode.CustomFormatScore), Release = new WebhookGrabbedRelease(message.Release) }; diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs new file mode 100644 index 000000000..5e8b47870 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookDownloadStatusMessage.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Download.TrackedDownloads; + +namespace NzbDrone.Core.Notifications.Webhook +{ + public class WebhookDownloadStatusMessage + { + public string Title { get; set; } + public List Messages { get; set; } + + public WebhookDownloadStatusMessage(TrackedDownloadStatusMessage statusMessage) + { + Title = statusMessage.Title; + Messages = statusMessage.Messages.ToList(); + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs index fca226f4b..b217f9284 100644 --- a/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs +++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookManualInteractionPayload.cs @@ -10,6 +10,8 @@ namespace NzbDrone.Core.Notifications.Webhook public string DownloadClient { get; set; } public string DownloadClientType { get; set; } public string DownloadId { get; set; } + public string DownloadStatus { get; set; } + public List DownloadStatusMessages { get; set; } public WebhookCustomFormatInfo CustomFormatInfo { get; set; } public WebhookGrabbedRelease Release { get; set; } } diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs index 028a64a18..7f5ddb780 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcJsonApiProxy.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Notifications.Xbmc private string ProcessRequest(XbmcSettings settings, string method, params object[] parameters) { - var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc"); + var url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase); var requestBuilder = new JsonRpcRequestBuilder(url, method, parameters); requestBuilder.LogResponseContent = true; diff --git a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs index 2d54157f2..97331f333 100644 --- a/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs +++ b/src/NzbDrone.Core/Notifications/Xbmc/XbmcSettings.cs @@ -14,16 +14,18 @@ namespace NzbDrone.Core.Notifications.Xbmc { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.DisplayTime).GreaterThanOrEqualTo(2); + RuleFor(c => c.UrlBase).ValidUrlBase(); } } public class XbmcSettings : IProviderConfig { - private static readonly XbmcSettingsValidator Validator = new XbmcSettingsValidator(); + private static readonly XbmcSettingsValidator Validator = new (); public XbmcSettings() { Port = 8080; + UrlBase = "/jsonrpc"; DisplayTime = 5; } @@ -65,7 +67,7 @@ namespace NzbDrone.Core.Notifications.Xbmc public bool AlwaysUpdate { get; set; } [JsonIgnore] - public string Address => $"{Host.ToUrlHost()}:{Port}"; + public string Address => $"{Host.ToUrlHost()}:{Port}{UrlBase}"; public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f527dd334..29cab2edb 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -47,13 +47,13 @@ namespace NzbDrone.Core.Organizer private readonly ICached _patternHasEpisodeIdentifierCache; private readonly Logger _logger; - private static readonly Regex TitleRegex = new Regex(@"(?\{\{|\}\})|\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[a-z0-9+-]+(?[- ._)\]]*)\}", + private static readonly Regex TitleRegex = new Regex(@"(?\{\{|\}\})|\{(?[- ._\[(]*)(?(?:[a-z0-9]+)(?:(?[- ._]+)(?:[a-z0-9]+))?)(?::(?[ ,a-z0-9+-]+(?[- ._)\]]*)\}", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - private static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", + public static readonly Regex EpisodeRegex = new Regex(@"(?\{episode(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", + public static readonly Regex SeasonRegex = new Regex(@"(?\{season(?:\:0+)?})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex AbsoluteEpisodeRegex = new Regex(@"(?\{absolute(?:\:0+)?})", @@ -622,7 +622,8 @@ namespace NzbDrone.Core.Organizer { tokenHandlers["{Original Title}"] = m => GetOriginalTitle(episodeFile, useCurrentFilenameAsFallback); tokenHandlers["{Original Filename}"] = m => GetOriginalFileName(episodeFile, useCurrentFilenameAsFallback); - tokenHandlers["{Release Group}"] = m => Truncate(episodeFile.ReleaseGroup, m.CustomFormat) ?? m.DefaultValue("Sonarr"); + tokenHandlers["{Release Group}"] = m => episodeFile.ReleaseGroup.IsNullOrWhiteSpace() ? m.DefaultValue("Sonarr") : Truncate(episodeFile.ReleaseGroup, m.CustomFormat); + tokenHandlers["{Release Hash}"] = m => episodeFile.ReleaseHash ?? string.Empty; } private void AddQualityTokens(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile) @@ -697,7 +698,16 @@ namespace NzbDrone.Core.Organizer customFormats = _formatCalculator.ParseCustomFormat(episodeFile, series); } - tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming)); + tokenHandlers["{Custom Formats}"] = m => GetCustomFormatsToken(customFormats, m.CustomFormat); + tokenHandlers["{Custom Format}"] = m => + { + if (m.CustomFormat.IsNullOrWhiteSpace()) + { + return string.Empty; + } + + return customFormats.FirstOrDefault(x => x.IncludeCustomFormatWhenRenaming && x.Name == m.CustomFormat)?.ToString() ?? string.Empty; + }; } private void AddIdTokens(Dictionary> tokenHandlers, Series series) @@ -707,6 +717,29 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{TvMazeId}"] = m => series.TvMazeId > 0 ? series.TvMazeId.ToString() : string.Empty; } + private string GetCustomFormatsToken(List customFormats, string filter) + { + var tokens = customFormats.Where(x => x.IncludeCustomFormatWhenRenaming).ToList(); + + var filteredTokens = tokens; + + if (filter.IsNotNullOrWhiteSpace()) + { + if (filter.StartsWith("-")) + { + var splitFilter = filter.Substring(1).Split(','); + filteredTokens = tokens.Where(c => !splitFilter.Contains(c.Name)).ToList(); + } + else + { + var splitFilter = filter.Split(','); + filteredTokens = tokens.Where(c => splitFilter.Contains(c.Name)).ToList(); + } + } + + return string.Join(" ", filteredTokens); + } + private string GetLanguagesToken(List mediaInfoLanguages, string filter, bool skipEnglishOnly, bool quoted) { var tokens = new List(); @@ -1168,6 +1201,11 @@ namespace NzbDrone.Core.Organizer private string Truncate(string input, string formatter) { + if (input.IsNullOrWhiteSpace()) + { + return string.Empty; + } + var maxLength = GetMaxLengthFromFormatter(formatter); if (maxLength == 0 || input.Length <= Math.Abs(maxLength)) diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index bcda9c884..e8d39469f 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -75,6 +75,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } } @@ -91,6 +92,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AirDateRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } @@ -109,6 +111,7 @@ namespace NzbDrone.Core.Organizer } return FileNameBuilder.SeasonEpisodePatternRegex.IsMatch(value) || + (FileNameBuilder.SeasonRegex.IsMatch(value) && FileNameBuilder.EpisodeRegex.IsMatch(value)) || FileNameBuilder.AbsoluteEpisodePatternRegex.IsMatch(value) || FileNameValidation.OriginalTokenRegex.IsMatch(value); } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs index 9367c11d8..8a50137fd 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidationService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidationService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.IO; using System.Linq; using FluentValidation.Results; using NzbDrone.Core.Parser.Model; @@ -20,7 +21,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateStandardFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("StandardEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -38,7 +41,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateDailyFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("DailyEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { @@ -66,7 +71,9 @@ namespace NzbDrone.Core.Organizer public ValidationFailure ValidateAnimeFilename(SampleResult sampleResult) { var validationFailure = new ValidationFailure("AnimeEpisodeFormat", ERROR_MESSAGE); - var parsedEpisodeInfo = Parser.Parser.ParseTitle(sampleResult.FileName); + var parsedEpisodeInfo = sampleResult.FileName.Contains(Path.DirectorySeparatorChar) + ? Parser.Parser.ParsePath(sampleResult.FileName) + : Parser.Parser.ParseTitle(sampleResult.FileName); if (parsedEpisodeInfo == null) { diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index d1c995b87..4071539c0 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex SubtitleLanguageTitleRegex = new Regex(@".+?(\.((?forced|foreign|default|cc|psdh|sdh)|(?[a-z]{2,3})))*[-_. ](?[^.]*)(\.((?<tags2>forced|foreign|default|cc|psdh|sdh)|(?<iso_code>[a-z]{2,3})))*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex SubtitleTitleRegex = new Regex(@"((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled); + private static readonly Regex SubtitleTitleRegex = new Regex(@"^((?<title>.+) - )?(?<copy>(?<!\d+)\d{1,3}(?!\d+))$", RegexOptions.Compiled); public static List<Language> ParseLanguages(string title) { diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index af7c7347c..0129c5d0c 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -37,13 +37,14 @@ namespace NzbDrone.Core.Parser.Model public bool ExistingFile { get; set; } public bool SceneSource { get; set; } public string ReleaseGroup { get; set; } + public string ReleaseHash { get; set; } public string SceneName { get; set; } public bool OtherVideoFiles { get; set; } public List<CustomFormat> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public GrabbedReleaseInfo Release { get; set; } public bool ScriptImported { get; set; } - public bool FileRenamedAfterScriptImport { get; set; } + public string FileNameBeforeRename { get; set; } public bool ShouldImportExtras { get; set; } public List<string> PossibleExtraFiles { get; set; } public SubtitleTitleInfo SubtitleInfo { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 3f4eeee25..f1a7bdcb4 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Core.Parser.Model public bool IsMultiSeason { get; set; } public bool IsSeasonExtra { get; set; } public bool IsSplitEpisode { get; set; } + public bool IsMiniSeries { get; set; } public bool Special { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 421ca70cf..c4f61fbce 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -83,11 +83,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Season+Episode - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:[_. ](?!\d+)).*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:[_. ](?!\d+)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Episode Absolute Episode Number ([SubGroup] Series Title Episode 01) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)(?<title>.+?)[-_. ]+?(?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number + Season+Episode @@ -95,39 +95,39 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Season+Episode + Absolute Episode Number - new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|\-[a-z])))+.*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\](?:_|-|\s|\.)?)(?<title>.+?)(?:[-_\W](?<![()\[!]))+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)(?:(?:_|-|\s|\.)+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|\-[a-z])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - Batch separated with tilde - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\s?~\s?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))\s?~\s?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with season number in brackets Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)[_. ]+?\(Season[_. ](?<season>\d+)\)[-_. ]+?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)[_. ]+?\(Season[_. ](?<season>\d+)\)[-_. ]+?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing 3-digit number and sub title - Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^]]+?)(?:[-_. ]{3}?(?<absoluteepisode>\d{2}(\.\d{1,2})?(?!-?\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title with trailing number Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+)[_ ]+)(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+)[_ ]+)(?:[-_. ]?(?<absoluteepisode>\d{3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title - Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?:[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-])))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number - Absolute Episode Number (batches without full separator between title and absolute episode numbers) - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)(?:(?<!\b[0]\d+))(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))[. ]-[. ](?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|[-]))(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - [SubGroup] Title Absolute Episode Number - new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)", + new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+|-[a-z]+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])?(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Multi-episode Repeated (S01E05 - S01E06) @@ -155,11 +155,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [SubGroup] [Hash]? (Series Title Episode 99-100 [RlsGroup] [ABCD1234]) - new Regex(@"^(?<title>.+?)[-_. ]Episode(?:[-_. ]+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?<title>.+?)[-_. ]Episode(?:[-_. ]+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [SubGroup] [Hash] - new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{3}(\.\d{1,2})(?!\d+)))+(?:.+?)\[(?<subgroup>.+?)\].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (Year) [SubGroup] @@ -167,11 +167,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title with trailing number, Absolute Episode Number and hash - new Regex(@"^(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])(?:$|\.mkv)", + new Regex(@"^(?<title>[^-]+?)(?:(?<![-_. ]|\b[0]\d+) - )(?:[-_. ]?(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>[(\[]\w{8}[)\]])(?:$|\.mkv)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number [Hash] - new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>\[\w{8}\])(?:$|\.)", + new Regex(@"^(?<title>.+?)(?:(?:_|-|\s|\.)+(?<absoluteepisode>\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:[-_. ]+(?<special>special|ova|ovd))?[-_. ]+.*?(?<hash>[(\[]\w{8}[)\]])$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Episodes with airdate AND season/episode number, capture season/episode only @@ -186,6 +186,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^((?<title>.*?)[ ._]\/[ ._])+\(?S(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:\W|_)?E?[ ._]?(?<episode>(?<!\d+)\d{1,2}(?!\d+))(?:-(?<episode>(?<!\d+)\d{1,2}(?!\d+)))?([ ._]of[ ._]\d+)?\)?[ ._][\(\[]", RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-episode with title (S01E99-100, S01E05-06) + new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{2,3}(?!\d+))(?:-(?<episode>\d{2,3}(?!\d+)))+(?:[-_. ]|$)", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + // Multi-episode with title (S01E05-06, S01E05-6) new Regex(@"^(?<title>.+?)(?:[-_\W](?<![()\[!]))+S(?<season>(?<!\d+)(?:\d{1,2})(?!\d+))E(?<episode>\d{1,2}(?!\d+))(?:-(?<episode>\d{1,2}(?!\d+)))+(?:[-_. ]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -290,10 +294,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)(?:_|-|\s|\.)+S(?<season>\d{2}(?!\d+))(\W-\W)E(?<episode>(?<!\d+)\d{2}(?!\d+))(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Season and episode numbers in square brackets (single and mult-episode) + // Season and episode numbers in square brackets (single and multi-episode) // Series Title - [02x01] - Episode 1 // Series Title - [02x01x02] - Episode 1 - new Regex(@"^(?<title>.+?)?(?:[-_\W](?<![()\[!]))+\[(?<season>(?<!\d+)\d{1,2})(?:(?:-|x){1,2}(?<episode>\d{2}))+\].+?(?:\.|$)", + new Regex(@"^(?<title>.+?)?(?:[-_\W](?<![()\[!]))+\[(?:s)?(?<season>(?<!\d+)\d{1,2})(?:(?:[ex])(?<episode>\d{2}))(?:(?:[-ex]){1,2}(?<episode>\d{2}))*\].+?(?:\.|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title with season number - Absolute Episode Number (Title S01 - EP14) @@ -324,10 +328,6 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)[-_. ]+?(?:S|Season|Saison|Series|Stagione)[-_. ]?(?<season>\d{4}(?![-_. ]?\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Episodes with a title and season/episode in square brackets - new Regex(@"^(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+\[S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>(?<!\d+)\d{2}(?!\d+|i|p)))+\])\W?(?!\\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - // Supports 103/113 naming new Regex(@"^(?<title>.+?)?(?:(?:[_.-](?<![()\[!]))+(?<season>(?<!\d+)[1-9])(?<episode>[1-9][0-9]|[0][1-9])(?![a-z]|\d+))+(?:[_.]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -358,7 +358,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (E195 or E1206) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>(\d{3}|\d{4})(\.\d{1,2})?))+[-_. ].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>(\d{3}|\d{4})(\.\d{1,2})?))+[-_. ].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Supports 1103/1113 naming @@ -386,27 +386,35 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime Range - Title Absolute Episode Number (ep01-12) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:_|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?)-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:_|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,3}(\.\d{1,2})?)-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number (e66) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,4}(\.\d{1,2})?))+[-_. ].*?(?<hash>\[\w{8}\])?(?:$|\.)", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:_|-|\s|\.)+(?:e|ep)(?<absoluteepisode>\d{2,4}(\.\d{1,2})?))+[-_. ].*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Episode Absolute Episode Number (Series Title Episode 01) - new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?<title>.+?)[-_. ](?:Episode)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+)))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime Range - Title Absolute Episode Number (1 or 2 digit absolute episode numbers in a range, 1-10) - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-))(?:_|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[_. ]+(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+))-(?<absoluteepisode>(?<!\d+)\d{1,2}(\.\d{1,2})?(?!\d+|-)).*?(?<hash>[(\[]\w{8}[)\]])?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + // Anime - Title Episode/Episodio Absolute Episode Number + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)[-_. ]+(?:Episode|Episodio)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + + // Anime - Title [Absolute Episode Number] from AniLibriaTV + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]\[)(?:(?:-?)(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+(?:\][-_. ]).*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title Absolute Episode Number - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:[-_. ]+(?<absoluteepisode>(?<!\d+)\d{2,4}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Anime - Title {Absolute Episode Number} - new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+(?:_|-|\s|\.)*?(?<hash>\[.{8}\])?(?:$|\.)?", + new Regex(@"^(?:\[(?<subgroup>.+?)\][-_. ]?)?(?<title>.+?)(?:(?:[-_\W](?<![()\[!]))+(?<absoluteepisode>(?<!\d+)\d{2,3}(\.\d{1,2})?(?!\d+|[ip])))+.*?(?<hash>[(\[]\w{8}[)\]])?$", RegexOptions.IgnoreCase | RegexOptions.Compiled), // Extant, terrible multi-episode naming (extant.10708.hdtv-lol.mp4) @@ -492,7 +500,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(8|10)b(it)?|10-bit)\s*?", + private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"(?:(480|540|576|720|1080|2160)[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*]|848x480|1280x720|1920x1080|3840x2160|4096x2160|(?<![a-f0-9])(8|10)(b(?![a-z0-9])|bit)|10-bit)\s*?", string.Empty, RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -533,7 +541,7 @@ namespace NzbDrone.Core.Parser private static readonly Regex ExceptionReleaseGroupRegexExact = new Regex(@"(?<releasegroup>(?:D\-Z0N3|Fight-BB|VARYG|E\.N\.D)\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); // groups whose releases end with RlsGroup) or RlsGroup] - private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<releasegroup>(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex ExceptionReleaseGroupRegex = new Regex(@"(?<=[._ \[])(?<releasegroup>(Silence|afm72|Panda|Ghost|MONOLITH|Tigole|Joy|ImE|UTR|t3nzin|Anime Time|Project Angel|Hakata Ramen|HONE|Vyndros|SEV|Garshasp|Kappa|Natty|RCVR|SAMPA|YOGI|r00t|EDGE2020|RZeroX)(?=\]|\)))", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)[-_. ]+?[\(\[]?(?<year>\d{4})[\]\)]?", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -546,6 +554,8 @@ namespace NzbDrone.Core.Parser private static readonly Regex ArticleWordRegex = new Regex(@"^(a|an|the)\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled); + private static readonly Regex SeasonFolderRegex = new Regex(@"^(?:S|Season|Saison|Series|Stagione)[-_. ]*(?<season>(?<!\d+)\d{1,4}(?!\d+))(?:[_. ]+(?!\d+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex SimpleEpisodeNumberRegex = new Regex(@"^[ex]?(?<episode>(?<!\d+)\d{1,3}(?!\d+))(?:[ex-](?<episode>(?<!\d+)\d{1,3}(?!\d+)))?(?:[_. ](?!\d+)(?<remaining>.+)|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled); @@ -554,9 +564,40 @@ namespace NzbDrone.Core.Parser public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); - var result = ParseTitle(fileInfo.Name); + // Parse using the folder and file separately, but combine if they both parse correctly. + var episodeNumberMatch = SimpleEpisodeNumberRegex.Match(fileInfo.Name); + + if (episodeNumberMatch.Success && fileInfo.Directory?.Name != null && (result == null || result.IsMiniSeries || result.AbsoluteEpisodeNumbers.Any())) + { + var seasonMatch = SeasonFolderRegex.Match(fileInfo.Directory.Name); + + if (seasonMatch.Success && seasonMatch.Groups["season"].Success) + { + var episodeCaptures = episodeNumberMatch.Groups["episode"].Captures.Cast<Capture>().ToList(); + var first = ParseNumber(episodeCaptures.First().Value); + var last = ParseNumber(episodeCaptures.Last().Value); + var pathTitle = $"S{seasonMatch.Groups["season"].Value}E{first:00}"; + + if (first != last) + { + pathTitle += $"-E{last:00}"; + } + + if (episodeNumberMatch.Groups["remaining"].Success) + { + pathTitle += $" {episodeNumberMatch.Groups["remaining"].Value}"; + } + + var parsedFileInfo = ParseTitle(pathTitle); + + Logger.Debug("Episode parsed from file and folder names. {0}", parsedFileInfo); + + return parsedFileInfo; + } + } + if (result == null && int.TryParse(Path.GetFileNameWithoutExtension(fileInfo.Name), out var number)) { Logger.Debug("Attempting to parse episode info using directory and file names. {0}", fileInfo.Directory.Name); @@ -1075,6 +1116,7 @@ namespace NzbDrone.Core.Parser { // If no season was found and it's not an absolute only release it should be treated as a mini series and season 1 result.SeasonNumber = 1; + result.IsMiniSeries = true; } } else @@ -1197,7 +1239,7 @@ namespace NzbDrone.Core.Parser if (hash.Success) { - var hashValue = hash.Value.Trim('[', ']'); + var hashValue = hash.Value.Trim('[', ']', '(', ')'); if (hashValue.Equals("1280x720")) { diff --git a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs index fba9ca3f3..09fecee2c 100644 --- a/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs +++ b/src/NzbDrone.Core/ProgressMessaging/ProgressMessageContext.cs @@ -1,10 +1,13 @@ -using System; +using System; +using System.Threading; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.ProgressMessaging { public static class ProgressMessageContext { + private static AsyncLocal<CommandModel> _commandModelAsync = new AsyncLocal<CommandModel>(); + [ThreadStatic] private static CommandModel _commandModel; @@ -13,8 +16,15 @@ namespace NzbDrone.Core.ProgressMessaging public static CommandModel CommandModel { - get { return _commandModel; } - set { _commandModel = value; } + get + { + return _commandModel ?? _commandModelAsync.Value; + } + set + { + _commandModel = value; + _commandModelAsync.Value = value; + } } public static bool LockReentrancy() diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj index d7a4c9fa5..e1e32454e 100644 --- a/src/NzbDrone.Core/Sonarr.Core.csproj +++ b/src/NzbDrone.Core/Sonarr.Core.csproj @@ -1,30 +1,30 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="Dapper" Version="2.0.123" /> + <PackageReference Include="Dapper" Version="2.1.28" /> <PackageReference Include="Diacritical.Net" Version="1.0.4" /> - <PackageReference Include="MailKit" Version="3.6.0" /> - <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.21" /> - <PackageReference Include="Polly" Version="8.2.0" /> + <PackageReference Include="MailKit" Version="4.3.0" /> + <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.2" /> + <PackageReference Include="Polly" Version="8.3.0" /> <PackageReference Include="Servarr.FFMpegCore" Version="4.7.0-26" /> <PackageReference Include="Servarr.FFprobe" Version="5.1.4.112" /> <PackageReference Include="System.Memory" Version="4.5.5" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" /> <PackageReference Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" /> - <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" /> + <PackageReference Include="FluentValidation" Version="11.9.0" /> + <PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.2.8" /> <PackageReference Include="MonoTorrent" Version="2.0.7" /> <PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" /> - <PackageReference Include="System.Text.Json" Version="6.0.8" /> - <PackageReference Include="Npgsql" Version="7.0.4" /> + <PackageReference Include="System.Text.Json" Version="8.0.2" /> + <PackageReference Include="Npgsql" Version="8.0.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index da67f0705..b97b279a5 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.ImportLists; @@ -120,7 +121,7 @@ namespace NzbDrone.Core.Tags var restrictions = _releaseProfileService.All(); var series = _seriesService.GetAllSeriesTags(); var indexers = _indexerService.All(); - var autotags = _autoTaggingService.All(); + var autoTags = _autoTaggingService.All(); var downloadClients = _downloadClientFactory.All(); var details = new List<TagDetails>(); @@ -137,7 +138,7 @@ namespace NzbDrone.Core.Tags RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), SeriesIds = series.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(), IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), - AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), + AutoTagIds = GetAutoTagIds(tag, autoTags), DownloadClientIds = downloadClients.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), }); } @@ -188,5 +189,23 @@ namespace NzbDrone.Core.Tags _repo.Delete(tagId); _eventAggregator.PublishEvent(new TagsUpdatedEvent()); } + + private List<int> GetAutoTagIds(Tag tag, List<AutoTag> autoTags) + { + var autoTagIds = autoTags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(); + + foreach (var autoTag in autoTags) + { + foreach (var specification in autoTag.Specifications) + { + if (specification is TagSpecification) + { + autoTagIds.Add(autoTag.Id); + } + } + } + + return autoTagIds.Distinct().ToList(); + } } } diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs index f985b96fb..4afa17f4d 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesService.cs @@ -78,13 +78,13 @@ namespace NzbDrone.Core.Tv series.Added = added; if (existingSeriesTvdbIds.Any(f => f == series.TvdbId)) { - _logger.Debug("TVDB ID {0} was not added due to validation failure: Series already exists in database", s.TvdbId); + _logger.Debug("TVDB ID {0} was not added due to validation failure: Series {1} already exists in database", s.TvdbId, s); continue; } if (seriesToAdd.Any(f => f.TvdbId == series.TvdbId)) { - _logger.Debug("TVDB ID {0} was not added due to validation failure: Series already exists on list", s.TvdbId); + _logger.Trace("TVDB ID {0} was already added from another import list, not adding series {1} again", s.TvdbId, s); continue; } @@ -104,7 +104,7 @@ namespace NzbDrone.Core.Tv throw; } - _logger.Debug("TVDB ID {0} was not added due to validation failures. {1}", s.TvdbId, ex.Message); + _logger.Debug("Series {0} with TVDB ID {1} was not added due to validation failures. {2}", s, s.TvdbId, ex.Message); } } @@ -121,7 +121,7 @@ namespace NzbDrone.Core.Tv } catch (SeriesNotFoundException) { - _logger.Error("TVDB ID {0} was not found, it may have been removed from TheTVDB. Path: {1}", newSeries.TvdbId, newSeries.Path); + _logger.Error("Series {0} with TVDB ID {1} was not found, it may have been removed from TheTVDB. Path: {2}", newSeries, newSeries.TvdbId, newSeries.Path); throw new ValidationException(new List<ValidationFailure> { diff --git a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs b/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs index da6e853dc..1c8a6f269 100644 --- a/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs +++ b/src/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs @@ -39,5 +39,7 @@ namespace NzbDrone.Core.Tv.Commands public override bool UpdateScheduledTask => SeriesIds.Empty(); public override bool IsLongRunning => true; + + public override string CompletionMessage => "Completed"; } } diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 241037799..20dad0582 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; @@ -42,16 +43,19 @@ namespace NzbDrone.Core.Tv public class EpisodeService : IEpisodeService, IHandle<EpisodeFileDeletedEvent>, IHandle<EpisodeFileAddedEvent>, - IHandleAsync<SeriesDeletedEvent> + IHandleAsync<SeriesDeletedEvent>, + IHandleAsync<SeriesScannedEvent> { private readonly IEpisodeRepository _episodeRepository; private readonly IConfigService _configService; + private readonly ICached<HashSet<int>> _cache; private readonly Logger _logger; - public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, Logger logger) + public EpisodeService(IEpisodeRepository episodeRepository, IConfigService configService, ICacheManager cacheManager, Logger logger) { _episodeRepository = episodeRepository; _configService = configService; + _cache = cacheManager.GetCache<HashSet<int>>(GetType()); _logger = logger; } @@ -215,34 +219,6 @@ namespace NzbDrone.Core.Tv _episodeRepository.DeleteMany(episodes); } - public void HandleAsync(SeriesDeletedEvent message) - { - var episodes = _episodeRepository.GetEpisodesBySeriesIds(message.Series.Select(s => s.Id).ToList()); - _episodeRepository.DeleteMany(episodes); - } - - public void Handle(EpisodeFileDeletedEvent message) - { - foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) - { - _logger.Debug("Detaching episode {0} from file.", episode.Id); - - var unmonitorForReason = message.Reason != DeleteMediaFileReason.Upgrade && - message.Reason != DeleteMediaFileReason.ManualOverride; - - _episodeRepository.ClearFileId(episode, unmonitorForReason && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes); - } - } - - public void Handle(EpisodeFileAddedEvent message) - { - foreach (var episode in message.EpisodeFile.Episodes.Value) - { - _episodeRepository.SetFileId(episode, message.EpisodeFile.Id); - _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); - } - } - private Episode FindOneByAirDate(int seriesId, string date, int? part) { var episodes = _episodeRepository.Find(seriesId, date); @@ -277,5 +253,73 @@ namespace NzbDrone.Core.Tv throw new InvalidOperationException($"Multiple episodes with the same air date found. Date: {date}"); } + + public void Handle(EpisodeFileDeletedEvent message) + { + foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) + { + _logger.Debug("Detaching episode {0} from file.", episode.Id); + + var unmonitorEpisodes = _configService.AutoUnmonitorPreviouslyDownloadedEpisodes; + + var unmonitorForReason = message.Reason != DeleteMediaFileReason.Upgrade && + message.Reason != DeleteMediaFileReason.ManualOverride && + message.Reason != DeleteMediaFileReason.MissingFromDisk; + + // If episode is being unlinked because it's missing from disk store it for + if (message.Reason == DeleteMediaFileReason.MissingFromDisk && unmonitorEpisodes) + { + lock (_cache) + { + var ids = _cache.Get(episode.SeriesId.ToString(), () => new HashSet<int>()); + + ids.Add(episode.Id); + } + } + + _episodeRepository.ClearFileId(episode, unmonitorForReason && unmonitorEpisodes); + } + } + + public void Handle(EpisodeFileAddedEvent message) + { + foreach (var episode in message.EpisodeFile.Episodes.Value) + { + _episodeRepository.SetFileId(episode, message.EpisodeFile.Id); + + lock (_cache) + { + var ids = _cache.Find(episode.SeriesId.ToString()); + + if (ids?.Contains(episode.Id) == true) + { + ids.Remove(episode.Id); + } + } + + _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); + } + } + + public void HandleAsync(SeriesDeletedEvent message) + { + var episodes = _episodeRepository.GetEpisodesBySeriesIds(message.Series.Select(s => s.Id).ToList()); + _episodeRepository.DeleteMany(episodes); + } + + public void HandleAsync(SeriesScannedEvent message) + { + lock (_cache) + { + var ids = _cache.Find(message.Series.Id.ToString()); + + if (ids?.Any() == true) + { + _episodeRepository.SetMonitored(ids, false); + } + + _cache.Remove(message.Series.Id.ToString()); + } + } } } diff --git a/src/NzbDrone.Core/Tv/MoveSeriesService.cs b/src/NzbDrone.Core/Tv/MoveSeriesService.cs index 49e245d11..be9037d96 100644 --- a/src/NzbDrone.Core/Tv/MoveSeriesService.cs +++ b/src/NzbDrone.Core/Tv/MoveSeriesService.cs @@ -1,6 +1,7 @@ using System.IO; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -51,6 +52,12 @@ namespace NzbDrone.Core.Tv _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", series.Title, sourcePath, destinationPath); } + if (sourcePath.PathEquals(destinationPath)) + { + _logger.ProgressInfo("{0} is already in the specified location '{1}'.", series, destinationPath); + return; + } + try { // Ensure the parent of the series folder exists, this will often just be the root folder, but diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 0ca1d8074..59a827a0b 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -6,7 +6,5 @@ namespace NzbDrone.Core.Update.Commands { public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; - - public override string CompletionMessage => null; } } diff --git a/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj b/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj index 00783be18..e62f81ff1 100644 --- a/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj +++ b/src/NzbDrone.Host.Test/Sonarr.Host.Test.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj index e11144bcf..a0492b532 100644 --- a/src/NzbDrone.Host/Sonarr.Host.csproj +++ b/src/NzbDrone.Host/Sonarr.Host.csproj @@ -1,15 +1,15 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Owin" Version="6.0.21" /> - <PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" /> + <PackageReference Include="Microsoft.AspNetCore.Owin" Version="8.0.2" /> + <PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" /> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj index b78253880..1ab50a049 100644 --- a/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/Sonarr.Integration.Test.csproj @@ -1,11 +1,11 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> - <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.21" /> + <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" /> diff --git a/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj b/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj index 511c54cba..b2f6c3002 100644 --- a/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj +++ b/src/NzbDrone.Libraries.Test/Sonarr.Libraries.Test.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> diff --git a/src/NzbDrone.Libraries.Test/app.config b/src/NzbDrone.Libraries.Test/app.config index d660afa84..8da9b15e4 100644 --- a/src/NzbDrone.Libraries.Test/app.config +++ b/src/NzbDrone.Libraries.Test/app.config @@ -3,6 +3,6 @@ <runtime> </runtime> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" /> </startup> </configuration> diff --git a/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj b/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj index 60c6f901c..6f067e499 100644 --- a/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj +++ b/src/NzbDrone.Mono.Test/Sonarr.Mono.Test.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <!-- The netstandard version here doesn't work in net framework diff --git a/src/NzbDrone.Mono.Test/app.config b/src/NzbDrone.Mono.Test/app.config index 85da475f2..a6d0b59f0 100644 --- a/src/NzbDrone.Mono.Test/app.config +++ b/src/NzbDrone.Mono.Test/app.config @@ -3,6 +3,6 @@ <runtime> </runtime> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" /> </startup> </configuration> \ No newline at end of file diff --git a/src/NzbDrone.Mono/Disk/FindDriveType.cs b/src/NzbDrone.Mono/Disk/FindDriveType.cs index d0481c3d4..08cc611de 100644 --- a/src/NzbDrone.Mono/Disk/FindDriveType.cs +++ b/src/NzbDrone.Mono/Disk/FindDriveType.cs @@ -6,15 +6,16 @@ namespace NzbDrone.Mono.Disk { public static class FindDriveType { - private static readonly Dictionary<string, DriveType> DriveTypeMap = new Dictionary<string, DriveType> - { - { "afpfs", DriveType.Network }, - { "apfs", DriveType.Fixed }, - { "fuse.mergerfs", DriveType.Fixed }, - { "fuse.glusterfs", DriveType.Network }, - { "nullfs", DriveType.Fixed }, - { "zfs", DriveType.Fixed } - }; + private static readonly Dictionary<string, DriveType> DriveTypeMap = new () + { + { "afpfs", DriveType.Network }, + { "apfs", DriveType.Fixed }, + { "fuse.mergerfs", DriveType.Fixed }, + { "fuse.shfs", DriveType.Fixed }, + { "fuse.glusterfs", DriveType.Network }, + { "nullfs", DriveType.Fixed }, + { "zfs", DriveType.Fixed } + }; public static DriveType Find(string driveFormat) { diff --git a/src/NzbDrone.Mono/Sonarr.Mono.csproj b/src/NzbDrone.Mono/Sonarr.Mono.csproj index 7901ccd16..ec9e47f03 100644 --- a/src/NzbDrone.Mono/Sonarr.Mono.csproj +++ b/src/NzbDrone.Mono/Sonarr.Mono.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <!-- diff --git a/src/NzbDrone.SignalR/Sonarr.SignalR.csproj b/src/NzbDrone.SignalR/Sonarr.SignalR.csproj index 19349baa6..0dbe8812b 100644 --- a/src/NzbDrone.SignalR/Sonarr.SignalR.csproj +++ b/src/NzbDrone.SignalR/Sonarr.SignalR.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> <OutputType>Library</OutputType> </PropertyGroup> <ItemGroup> diff --git a/src/NzbDrone.Test.Common/App.config b/src/NzbDrone.Test.Common/App.config index d1790b089..6c239bdb2 100644 --- a/src/NzbDrone.Test.Common/App.config +++ b/src/NzbDrone.Test.Common/App.config @@ -6,6 +6,6 @@ <runtime> </runtime> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" /> </startup> </configuration> diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 25f977fce..6caae84d3 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -55,7 +55,7 @@ namespace NzbDrone.Test.Common if (BuildInfo.IsDebug) { - Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "_output", "net6.0", consoleExe)); + Start(Path.Combine(TestContext.CurrentContext.TestDirectory, "..", "..", "_output", "net8.0", consoleExe)); } else { diff --git a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj index bc5f721f6..c4da41cfc 100644 --- a/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj +++ b/src/NzbDrone.Test.Common/Sonarr.Test.Common.csproj @@ -1,14 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.10.0" /> - <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="Moq" Version="4.18.4" /> - <PackageReference Include="NLog" Version="4.7.14" /> - <PackageReference Include="NUnit" Version="3.13.3" /> - <PackageReference Include="RestSharp" Version="106.15.0" /> + <PackageReference Include="FluentAssertions" Version="6.12.0" /> + <PackageReference Include="FluentValidation" Version="11.9.0" /> + <PackageReference Include="Moq" Version="4.20.70" /> + <PackageReference Include="NLog" Version="5.2.8" /> + <PackageReference Include="NUnit" Version="4.1.0" /> + <PackageReference Include="RestSharp" Version="110.2.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Test.Dummy/Sonarr.Test.Dummy.csproj b/src/NzbDrone.Test.Dummy/Sonarr.Test.Dummy.csproj index 9d17bf8f8..e9223712f 100644 --- a/src/NzbDrone.Test.Dummy/Sonarr.Test.Dummy.csproj +++ b/src/NzbDrone.Test.Dummy/Sonarr.Test.Dummy.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> </Project> \ No newline at end of file diff --git a/src/NzbDrone.Test.Dummy/app.config b/src/NzbDrone.Test.Dummy/app.config index ac5aa757c..f217137c1 100644 --- a/src/NzbDrone.Test.Dummy/app.config +++ b/src/NzbDrone.Test.Dummy/app.config @@ -1,3 +1,6 @@ <?xml version="1.0"?> <configuration> -<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration> + <startup> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1"/> + </startup> +</configuration> diff --git a/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj b/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj index 5b4d7dfd7..9f62e562a 100644 --- a/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj +++ b/src/NzbDrone.Update.Test/Sonarr.Update.Test.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> diff --git a/src/NzbDrone.Update/Sonarr.Update.csproj b/src/NzbDrone.Update/Sonarr.Update.csproj index ab05b2ca4..aa5c26c70 100644 --- a/src/NzbDrone.Update/Sonarr.Update.csproj +++ b/src/NzbDrone.Update/Sonarr.Update.csproj @@ -1,12 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="DryIoc.dll" Version="5.4.1" /> + <PackageReference Include="DryIoc.dll" Version="5.4.3" /> <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.2.8" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" /> diff --git a/src/NzbDrone.Update/app.config b/src/NzbDrone.Update/app.config index d660afa84..8da9b15e4 100644 --- a/src/NzbDrone.Update/app.config +++ b/src/NzbDrone.Update/app.config @@ -3,6 +3,6 @@ <runtime> </runtime> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" /> </startup> </configuration> diff --git a/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj b/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj index 15bcf5d51..8edec17bb 100644 --- a/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj +++ b/src/NzbDrone.Windows.Test/Sonarr.Windows.Test.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" /> diff --git a/src/NzbDrone.Windows.Test/app.config b/src/NzbDrone.Windows.Test/app.config index 85da475f2..a6d0b59f0 100644 --- a/src/NzbDrone.Windows.Test/app.config +++ b/src/NzbDrone.Windows.Test/app.config @@ -3,6 +3,6 @@ <runtime> </runtime> <startup> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" /> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1" /> </startup> </configuration> \ No newline at end of file diff --git a/src/NzbDrone.Windows/Sonarr.Windows.csproj b/src/NzbDrone.Windows/Sonarr.Windows.csproj index 96ab6ac02..5aeaf6216 100644 --- a/src/NzbDrone.Windows/Sonarr.Windows.csproj +++ b/src/NzbDrone.Windows/Sonarr.Windows.csproj @@ -1,10 +1,10 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> </PropertyGroup> <ItemGroup> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.2.8" /> <PackageReference Include="System.IO.FileSystem.AccessControl" Version="6.0.0-preview.5.21301.5" /> </ItemGroup> <ItemGroup> diff --git a/src/NzbDrone/Sonarr.csproj b/src/NzbDrone/Sonarr.csproj index 77a39e69c..097a28336 100644 --- a/src/NzbDrone/Sonarr.csproj +++ b/src/NzbDrone/Sonarr.csproj @@ -1,14 +1,14 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> - <TargetFrameworks>net6.0-windows</TargetFrameworks> + <TargetFrameworks>net8.0-windows</TargetFrameworks> <RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers> <UseWindowsForms>true</UseWindowsForms> <ApplicationIcon>..\NzbDrone.Host\Sonarr.ico</ApplicationIcon> <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources> </PropertyGroup> <ItemGroup> - <PackageReference Include="System.Resources.Extensions" Version="6.0.0" /> + <PackageReference Include="System.Resources.Extensions" Version="8.0.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" /> diff --git a/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj b/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj index 967ce008c..fa4014f43 100644 --- a/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj +++ b/src/ServiceHelpers/ServiceInstall/ServiceInstall.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Security.Principal.Windows" Version="6.0.0-preview.5.21301.5" /> diff --git a/src/ServiceHelpers/ServiceInstall/app.config b/src/ServiceHelpers/ServiceInstall/app.config index d0244c521..52652c147 100644 --- a/src/ServiceHelpers/ServiceInstall/app.config +++ b/src/ServiceHelpers/ServiceInstall/app.config @@ -1,6 +1,6 @@ <?xml version="1.0"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1"/> </startup> </configuration> diff --git a/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj b/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj index 967ce008c..fa4014f43 100644 --- a/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj +++ b/src/ServiceHelpers/ServiceUninstall/ServiceUninstall.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Security.Principal.Windows" Version="6.0.0-preview.5.21301.5" /> diff --git a/src/ServiceHelpers/ServiceUninstall/app.config b/src/ServiceHelpers/ServiceUninstall/app.config index d0244c521..52652c147 100644 --- a/src/ServiceHelpers/ServiceUninstall/app.config +++ b/src/ServiceHelpers/ServiceUninstall/app.config @@ -1,6 +1,6 @@ <?xml version="1.0"?> <configuration> <startup useLegacyV2RuntimeActivationPolicy="true"> - <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8.1"/> </startup> </configuration> diff --git a/src/Sonarr.Api.V3/Commands/CommandController.cs b/src/Sonarr.Api.V3/Commands/CommandController.cs index c5b892a98..1b17916de 100644 --- a/src/Sonarr.Api.V3/Commands/CommandController.cs +++ b/src/Sonarr.Api.V3/Commands/CommandController.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.Composition; using NzbDrone.Common.Serializer; using NzbDrone.Common.TPL; using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaFiles.EpisodeImport.Manual; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ProgressMessaging; @@ -61,6 +62,9 @@ namespace Sonarr.Api.V3.Commands using (var reader = new StreamReader(Request.Body)) { var body = reader.ReadToEnd(); + var priority = commandType == typeof(ManualImportCommand) + ? CommandPriority.High + : CommandPriority.Normal; dynamic command = STJson.Deserialize(body, commandType); @@ -69,7 +73,8 @@ namespace Sonarr.Api.V3.Commands command.SendUpdatesToClient = true; command.ClientUserAgent = Request.Headers["UserAgent"]; - var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + var trackedCommand = _commandQueueManager.Push(command, priority, CommandTrigger.Manual); + return Created(trackedCommand.Id); } } diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index d77338ea3..552a34326 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Http.REST; @@ -26,7 +27,7 @@ namespace Sonarr.Api.V3.EpisodeFiles public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int? IndexerFlags { get; set; } - public int? ReleaseType { get; set; } + public ReleaseType? ReleaseType { get; set; } public MediaInfoResource MediaInfo { get; set; } public bool QualityCutoffNotMet { get; set; } @@ -64,7 +65,7 @@ namespace Sonarr.Api.V3.EpisodeFiles CustomFormats = customFormats.ToResource(false), CustomFormatScore = customFormatScore, IndexerFlags = (int)model.IndexerFlags, - ReleaseType = (int)model.ReleaseType, + ReleaseType = model.ReleaseType, }; } } diff --git a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs index 4a701347c..d9cd55c03 100644 --- a/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs +++ b/src/Sonarr.Api.V3/ImportLists/ImportListExclusionController.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using FluentValidation; using Microsoft.AspNetCore.Mvc; using NzbDrone.Core.ImportLists.Exclusions; using Sonarr.Http; +using Sonarr.Http.Extensions; using Sonarr.Http.REST; using Sonarr.Http.REST.Attributes; @@ -29,11 +31,22 @@ namespace Sonarr.Api.V3.ImportLists [HttpGet] [Produces("application/json")] + [Obsolete("Deprecated")] public List<ImportListExclusionResource> GetImportListExclusions() { return _importListExclusionService.All().ToResource(); } + [HttpGet("paged")] + [Produces("application/json")] + public PagingResource<ImportListExclusionResource> GetImportListExclusionsPaged([FromQuery] PagingRequestResource paging) + { + var pagingResource = new PagingResource<ImportListExclusionResource>(paging); + var pageSpec = pagingResource.MapToPagingSpec<ImportListExclusionResource, ImportListExclusion>(); + + return pageSpec.ApplyToPage(_importListExclusionService.Paged, ImportListExclusionResourceMapper.ToResource); + } + [RestPostById] [Consumes("application/json")] public ActionResult<ImportListExclusionResource> AddImportListExclusion(ImportListExclusionResource resource) diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs index b36aedc41..1b2f22417 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushController.cs @@ -41,7 +41,8 @@ namespace Sonarr.Api.V3.Indexers _logger = logger; PostValidator.RuleFor(s => s.Title).NotEmpty(); - PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty().When(s => s.MagnetUrl.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.MagnetUrl).NotEmpty().When(s => s.DownloadUrl.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.Protocol).NotEmpty(); PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); } @@ -50,7 +51,7 @@ namespace Sonarr.Api.V3.Indexers [Consumes("application/json")] public ActionResult<List<ReleaseResource>> Create(ReleaseResource release) { - _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); + _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl ?? release.MagnetUrl); ValidateResource(release); diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs index f537f2e2f..46ab91a95 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportController.cs @@ -39,10 +39,11 @@ namespace Sonarr.Api.V3.ManualImport { foreach (var item in items) { - var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags); + var processedItem = _manualImportService.ReprocessItem(item.Path, item.DownloadId, item.SeriesId, item.SeasonNumber, item.EpisodeIds ?? new List<int>(), item.ReleaseGroup, item.Quality, item.Languages, item.IndexerFlags, item.ReleaseType); item.SeasonNumber = processedItem.SeasonNumber; item.Episodes = processedItem.Episodes.ToResource(); + item.ReleaseType = processedItem.ReleaseType; item.IndexerFlags = processedItem.IndexerFlags; item.Rejections = processedItem.Rejections; item.CustomFormats = processedItem.CustomFormats.ToResource(false); diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs index 66bb78ba9..4eb2bbe4b 100644 --- a/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportReprocessResource.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using Sonarr.Api.V3.CustomFormats; using Sonarr.Api.V3.Episodes; @@ -22,6 +23,7 @@ namespace Sonarr.Api.V3.ManualImport public List<CustomFormatResource> CustomFormats { get; set; } public int CustomFormatScore { get; set; } public int IndexerFlags { get; set; } + public ReleaseType ReleaseType { get; set; } public IEnumerable<Rejection> Rejections { get; set; } } } diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index 332f902ee..1d4c81068 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -1,11 +1,11 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentValidation" Version="9.5.4" /> + <PackageReference Include="FluentValidation" Version="11.9.0" /> <PackageReference Include="Ical.Net" Version="4.2.0" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="NLog" Version="5.2.8" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" /> diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index e500eba23..14444bdba 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -3070,7 +3070,8 @@ } } } - } + }, + "deprecated": true }, "post": { "tags": [ @@ -3109,6 +3110,59 @@ } } }, + "/api/v3/importlistexclusion/paged": { + "get": { + "tags": [ + "ImportListExclusion" + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 10 + } + }, + { + "name": "sortKey", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "sortDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SortDirection" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportListExclusionResourcePagingResource" + } + } + } + } + } + } + }, "/api/v3/importlistexclusion/{id}": { "put": { "tags": [ @@ -5089,6 +5143,23 @@ } } } + }, + "head": { + "tags": [ + "Ping" + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResource" + } + } + } + } + } } }, "/api/v3/qualitydefinition/{id}": { @@ -8221,9 +8292,7 @@ "nullable": true }, "releaseType": { - "type": "integer", - "format": "int32", - "nullable": true + "$ref": "#/components/schemas/ReleaseType" }, "mediaInfo": { "$ref": "#/components/schemas/MediaInfoResource" @@ -8889,6 +8958,38 @@ }, "additionalProperties": false }, + "ImportListExclusionResourcePagingResource": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + }, + "sortKey": { + "type": "string", + "nullable": true + }, + "sortDirection": { + "$ref": "#/components/schemas/SortDirection" + }, + "totalRecords": { + "type": "integer", + "format": "int32" + }, + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportListExclusionResource" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "ImportListResource": { "type": "object", "properties": { @@ -9442,6 +9543,9 @@ "type": "integer", "format": "int32" }, + "releaseType": { + "$ref": "#/components/schemas/ReleaseType" + }, "rejections": { "type": "array", "items": { @@ -10128,6 +10232,9 @@ "isSplitEpisode": { "type": "boolean" }, + "isMiniSeries": { + "type": "boolean" + }, "special": { "type": "boolean" }, diff --git a/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs index 864dd3644..af1caf833 100644 --- a/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs +++ b/src/Sonarr.Http/Authentication/ApiKeyAuthenticationHandler.cs @@ -28,9 +28,8 @@ namespace Sonarr.Http.Authentication public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, - ISystemClock clock, IConfigFileProvider config) - : base(options, logger, encoder, clock) + : base(options, logger, encoder) { _apiKey = config.ApiKey; } diff --git a/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs index 41dc5aad2..66333f14b 100644 --- a/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs +++ b/src/Sonarr.Http/Authentication/BasicAuthenticationHandler.cs @@ -6,6 +6,7 @@ using System.Text.Encodings.Web; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NzbDrone.Common.EnvironmentInfo; @@ -20,9 +21,8 @@ namespace Sonarr.Http.Authentication public BasicAuthenticationHandler(IAuthenticationService authService, IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) - : base(options, logger, encoder, clock) + UrlEncoder encoder) + : base(options, logger, encoder) { _authService = authService; } @@ -71,7 +71,7 @@ namespace Sonarr.Http.Authentication protected override Task HandleChallengeAsync(AuthenticationProperties properties) { - Response.Headers.Add("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\""); + Response.Headers.Append("WWW-Authenticate", $"Basic realm=\"{BuildInfo.AppName}\""); Response.StatusCode = 401; return Task.CompletedTask; } diff --git a/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs b/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs index da1c8b04b..7c62a1c8b 100644 --- a/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs +++ b/src/Sonarr.Http/Authentication/NoAuthenticationHandler.cs @@ -13,9 +13,8 @@ namespace Sonarr.Http.Authentication { public NoAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, - UrlEncoder encoder, - ISystemClock clock) - : base(options, logger, encoder, clock) + UrlEncoder encoder) + : base(options, logger, encoder) { } diff --git a/src/Sonarr.Http/Middleware/VersionMiddleware.cs b/src/Sonarr.Http/Middleware/VersionMiddleware.cs index abb443944..579138df9 100644 --- a/src/Sonarr.Http/Middleware/VersionMiddleware.cs +++ b/src/Sonarr.Http/Middleware/VersionMiddleware.cs @@ -22,7 +22,7 @@ namespace Sonarr.Http.Middleware { if (context.Request.IsApiRequest() && !context.Response.Headers.ContainsKey(VERSIONHEADER)) { - context.Response.Headers.Add(VERSIONHEADER, _version); + context.Response.Headers.Append(VERSIONHEADER, _version); } await _next(context); diff --git a/src/Sonarr.Http/Ping/PingController.cs b/src/Sonarr.Http/Ping/PingController.cs index c1b9c02fc..091e391cf 100644 --- a/src/Sonarr.Http/Ping/PingController.cs +++ b/src/Sonarr.Http/Ping/PingController.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Http [AllowAnonymous] [HttpGet("/ping")] + [HttpHead("/ping")] [Produces("application/json")] public ActionResult<PingResource> GetStatus() { diff --git a/src/Sonarr.Http/REST/RestController.cs b/src/Sonarr.Http/REST/RestController.cs index 7632d8b7f..830b00e6b 100644 --- a/src/Sonarr.Http/REST/RestController.cs +++ b/src/Sonarr.Http/REST/RestController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using FluentValidation; using FluentValidation.Results; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; @@ -102,7 +103,7 @@ namespace Sonarr.Http.REST if (controllerAttributes.Any(x => x.AttributeType == DEPRECATED_ATTRIBUTE) || attributes.Any(x => x.AttributeType == DEPRECATED_ATTRIBUTE)) { _logger.Warn("API call made to deprecated endpoint from {0}", Request.Headers.UserAgent.ToString()); - Response.Headers.Add("Deprecation", "true"); + Response.Headers.Append("Deprecation", "true"); } base.OnActionExecuting(context); diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj index 6c0adc7d8..f9f3cae9f 100644 --- a/src/Sonarr.Http/Sonarr.Http.csproj +++ b/src/Sonarr.Http/Sonarr.Http.csproj @@ -1,11 +1,11 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentValidation" Version="9.5.4" /> - <PackageReference Include="ImpromptuInterface" Version="7.0.1" /> - <PackageReference Include="NLog" Version="4.7.14" /> + <PackageReference Include="FluentValidation" Version="11.9.0" /> + <PackageReference Include="ImpromptuInterface" Version="8.0.4" /> + <PackageReference Include="NLog" Version="5.2.8" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NzbDrone.Core\Sonarr.Core.csproj" /> diff --git a/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj b/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj index 7cf363008..75c29d006 100644 --- a/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj +++ b/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj @@ -1,8 +1,8 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net6.0</TargetFrameworks> + <TargetFrameworks>net8.0</TargetFrameworks> </PropertyGroup> <ItemGroup> - <PackageReference Include="Lib.Harmony" Version="2.0.1" /> + <PackageReference Include="Lib.Harmony" Version="2.3.0" /> </ItemGroup> </Project> diff --git a/src/coverlet.runsettings b/src/coverlet.runsettings index 4c75a674d..b75b2f3a4 100644 --- a/src/coverlet.runsettings +++ b/src/coverlet.runsettings @@ -12,7 +12,7 @@ </DataCollectionRunSettings> <InProcDataCollectionRunSettings> <InProcDataCollectors> - <InProcDataCollector assemblyQualifiedName="Coverlet.Collector.DataCollection.CoverletInProcDataCollector, coverlet.collector, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null" + <InProcDataCollector assemblyQualifiedName="Coverlet.Collector.DataCollection.CoverletInProcDataCollector, coverlet.collector, Version=6.0.1.0, Culture=neutral, PublicKeyToken=null" friendlyName="XPlat Code Coverage" enabled="True" codebase="coverlet.collector.dll" /> diff --git a/test.sh b/test.sh index f2f376451..4b10bf85d 100644 --- a/test.sh +++ b/test.sh @@ -1,4 +1,5 @@ #! /bin/bash + PLATFORM=$1 TYPE=$2 COVERAGE=$3 diff --git a/yarn.lock b/yarn.lock index 9c7bf1ffa..4985fbd6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1207,14 +1207,14 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== -"@microsoft/signalr@6.0.21": - version "6.0.21" - resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-6.0.21.tgz#b45f335df7011abba831cb3d7974b58da7e725c7" - integrity sha512-3MWhSUE7AxkQs3QBuJ/spJJpg1mAHo0/6yRGhs5+Hew3Z+iqYrHVfo0yTElC7W2bVA9t3fW3jliQ9rBN0OvJLA== +"@microsoft/signalr@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-8.0.0.tgz#cb1412e88e0527f40da9178fefc27a65c3ddeab0" + integrity sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA== dependencies: abort-controller "^3.0.0" - eventsource "^1.0.7" - fetch-cookie "^0.11.0" + eventsource "^2.0.2" + fetch-cookie "^2.0.3" node-fetch "^2.6.7" ws "^7.4.5" @@ -3189,10 +3189,10 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource@^1.0.7: - version "1.1.2" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2" - integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== +eventsource@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" @@ -3257,12 +3257,13 @@ fbjs@^0.8.0: setimmediate "^1.0.5" ua-parser-js "^0.7.30" -fetch-cookie@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.11.0.tgz#e046d2abadd0ded5804ce7e2cae06d4331c15407" - integrity sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA== +fetch-cookie@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz#01086b6b5b1c3e08f15ffd8647b02ca100377365" + integrity sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ== dependencies: - tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" + set-cookie-parser "^2.4.8" + tough-cookie "^4.0.0" file-entry-cache@^6.0.1: version "6.0.1" @@ -5902,6 +5903,11 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +set-cookie-parser@^2.4.8: + version "2.6.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" + integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -6388,7 +6394,7 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": +tough-cookie@^4.0.0: version "4.1.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==